arrayview 0.9.0__tar.gz → 0.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {arrayview-0.9.0 → arrayview-0.10.0}/PKG-INFO +1 -1
- {arrayview-0.9.0 → arrayview-0.10.0}/pyproject.toml +1 -1
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/_server.py +84 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/_viewer.html +522 -86
- {arrayview-0.9.0 → arrayview-0.10.0}/uv.lock +1 -1
- {arrayview-0.9.0 → arrayview-0.10.0}/.claude/skills/invocation-consistency/SKILL.md +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/.claude/skills/modes-consistency/SKILL.md +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/.claude/skills/ui-consistency-audit/SKILL.md +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/.claude/skills/viewer-ui-checklist/SKILL.md +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/.claude/skills/visual-bug-fixing/SKILL.md +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/.github/workflows/docs.yml +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/.github/workflows/python-publish.yml +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/.gitignore +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/.python-version +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/.tmp-vsix/extension/extension.js +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/.tmp-vsix/extension/package.json +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/AGENTS.md +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/LICENSE +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/README.md +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/docs/comparing.md +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/docs/configuration.md +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/docs/display.md +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/docs/index.md +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/docs/loading.md +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/docs/logo.png +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/docs/measurement.md +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/docs/remote.md +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/docs/stylesheets/extra.css +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/docs/viewing.md +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/matlab/arrayview.m +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/mkdocs.yml +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/plans/webview/LOG.md +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/scripts/demo.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/scripts/release.sh +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/__init__.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/__main__.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/_app.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/_config.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/_icon.png +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/_io.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/_launcher.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/_platform.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/_render.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/_session.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/_shell.html +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/_stdio_server.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/_torch.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/tests/conftest.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/tests/test_api.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/tests/test_browser.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/tests/test_cli.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/tests/test_config.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/tests/test_interactions.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/tests/test_large_arrays.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/tests/test_torch.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/tests/ui_audit.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/tests/visual_smoke.py +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/vscode-extension/LICENSE +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/vscode-extension/extension.js +0 -0
- {arrayview-0.9.0 → arrayview-0.10.0}/vscode-extension/package.json +0 -0
|
@@ -1913,6 +1913,90 @@ def get_volume_histogram(
|
|
|
1913
1913
|
return result
|
|
1914
1914
|
|
|
1915
1915
|
|
|
1916
|
+
# ── Volume Data (3-D MIP) ────────────────────────────────────────
|
|
1917
|
+
|
|
1918
|
+
|
|
1919
|
+
@app.get("/volume_data/{sid}")
|
|
1920
|
+
def get_volume_data(
|
|
1921
|
+
sid: str,
|
|
1922
|
+
dims: str = "",
|
|
1923
|
+
indices: str = "",
|
|
1924
|
+
complex_mode: int = 0,
|
|
1925
|
+
):
|
|
1926
|
+
"""Return the full 3-D sub-volume as raw float32 bytes for the WebGL MIP renderer.
|
|
1927
|
+
|
|
1928
|
+
*dims* is a comma-separated triple of the three spatial dimensions (e.g. ``"0,1,2"``).
|
|
1929
|
+
*indices* gives the current index for every dimension (comma-separated).
|
|
1930
|
+
Dimensions not in *dims* are sliced at the value given in *indices*.
|
|
1931
|
+
|
|
1932
|
+
If any axis exceeds 256, stride-based down-sampling is applied so that
|
|
1933
|
+
the returned volume fits within 256^3.
|
|
1934
|
+
|
|
1935
|
+
Response headers carry ``X-Shape`` (comma-separated), ``X-Vmin``, ``X-Vmax``.
|
|
1936
|
+
"""
|
|
1937
|
+
session = SESSIONS.get(sid)
|
|
1938
|
+
if not session:
|
|
1939
|
+
return Response(status_code=404)
|
|
1940
|
+
|
|
1941
|
+
# Parse dims and indices
|
|
1942
|
+
if not dims:
|
|
1943
|
+
return Response(status_code=400, content="dims required")
|
|
1944
|
+
dim_list = [int(d) for d in dims.split(",")]
|
|
1945
|
+
if len(dim_list) != 3:
|
|
1946
|
+
return Response(status_code=400, content="dims must be exactly 3 integers")
|
|
1947
|
+
|
|
1948
|
+
idx_list = [int(x) for x in indices.split(",")] if indices else [s // 2 for s in session.shape]
|
|
1949
|
+
|
|
1950
|
+
# Build slicer: keep the 3 spatial dims free, fix everything else
|
|
1951
|
+
slicer = []
|
|
1952
|
+
for i in range(len(session.shape)):
|
|
1953
|
+
if i in dim_list:
|
|
1954
|
+
slicer.append(slice(None))
|
|
1955
|
+
else:
|
|
1956
|
+
idx = idx_list[i] if i < len(idx_list) else session.shape[i] // 2
|
|
1957
|
+
slicer.append(min(max(idx, 0), session.shape[i] - 1))
|
|
1958
|
+
vol = np.array(session.data[tuple(slicer)])
|
|
1959
|
+
|
|
1960
|
+
# Reorder axes so dim_list order maps to (0, 1, 2) of the output
|
|
1961
|
+
# vol currently has axes at the positions corresponding to dim_list within
|
|
1962
|
+
# the free-axis subset. We need to figure out which axes of `vol` correspond
|
|
1963
|
+
# to which dim in dim_list.
|
|
1964
|
+
free_axes_sorted = sorted(dim_list)
|
|
1965
|
+
# vol axes are in the order of free_axes_sorted (numpy preserves order)
|
|
1966
|
+
perm = [free_axes_sorted.index(d) for d in dim_list]
|
|
1967
|
+
vol = np.transpose(vol, perm)
|
|
1968
|
+
|
|
1969
|
+
# Apply complex mode
|
|
1970
|
+
vol = apply_complex_mode(vol, complex_mode)
|
|
1971
|
+
|
|
1972
|
+
# Downsample any axis > 256
|
|
1973
|
+
max_dim = 256
|
|
1974
|
+
strides = []
|
|
1975
|
+
for s in vol.shape:
|
|
1976
|
+
strides.append(max(1, (s + max_dim - 1) // max_dim))
|
|
1977
|
+
if any(st > 1 for st in strides):
|
|
1978
|
+
vol = vol[::strides[0], ::strides[1], ::strides[2]]
|
|
1979
|
+
|
|
1980
|
+
vol = np.ascontiguousarray(vol, dtype=np.float32)
|
|
1981
|
+
|
|
1982
|
+
finite = vol[np.isfinite(vol)]
|
|
1983
|
+
if finite.size > 0:
|
|
1984
|
+
vmin = float(np.percentile(finite, 1))
|
|
1985
|
+
vmax = float(np.percentile(finite, 99))
|
|
1986
|
+
else:
|
|
1987
|
+
vmin, vmax = 0.0, 1.0
|
|
1988
|
+
|
|
1989
|
+
return Response(
|
|
1990
|
+
content=vol.tobytes(),
|
|
1991
|
+
media_type="application/octet-stream",
|
|
1992
|
+
headers={
|
|
1993
|
+
"X-Shape": ",".join(str(s) for s in vol.shape),
|
|
1994
|
+
"X-Vmin": str(vmin),
|
|
1995
|
+
"X-Vmax": str(vmax),
|
|
1996
|
+
},
|
|
1997
|
+
)
|
|
1998
|
+
|
|
1999
|
+
|
|
1916
2000
|
@app.get("/lebesgue/{sid}")
|
|
1917
2001
|
def get_lebesgue_slice(
|
|
1918
2002
|
sid: str,
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
border-color: rgba(216, 222, 233, 0.07);
|
|
84
84
|
}
|
|
85
85
|
#viewer-row {
|
|
86
|
+
position: relative;
|
|
86
87
|
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
|
87
88
|
width: 100%; padding: 8px; box-sizing: border-box;
|
|
88
89
|
}
|
|
@@ -462,6 +463,8 @@
|
|
|
462
463
|
border: 1px solid rgba(210,90,200,0.25); }
|
|
463
464
|
.mode-badge-diff { background: rgba(255,130,90,0.15); color: #ff8a5c;
|
|
464
465
|
border: 1px solid rgba(255,130,90,0.25); }
|
|
466
|
+
.mode-badge-mip { background: rgba(0,200,180,0.15); color: #4fd1c5;
|
|
467
|
+
border-color: rgba(0,200,180,0.3); }
|
|
465
468
|
.mode-badge-proj { background: rgba(140,100,240,0.15); color: #a78bfa;
|
|
466
469
|
border: 1px solid rgba(140,100,240,0.25); }
|
|
467
470
|
:root.light .mode-badge-fft { background: rgba(40,160,100,0.1); color: #288c64; border-color: rgba(40,160,100,0.25); }
|
|
@@ -470,6 +473,7 @@
|
|
|
470
473
|
:root.light .mode-badge-alpha { background: rgba(180,50,50,0.1); color: #c04040; border-color: rgba(180,50,50,0.25); }
|
|
471
474
|
:root.light .mode-badge-rgb { background: rgba(170,50,160,0.1); color: #a030a0; border-color: rgba(170,50,160,0.25); }
|
|
472
475
|
:root.light .mode-badge-diff { background: rgba(200,100,50,0.1); color: #b05520; border-color: rgba(200,100,50,0.25); }
|
|
476
|
+
:root.light .mode-badge-mip { background: rgba(0,180,160,0.1); color: #108080; border-color: rgba(0,180,160,0.25); }
|
|
473
477
|
:root.light .mode-badge-proj { background: rgba(120,80,200,0.1); color: #7040c0; border-color: rgba(120,80,200,0.25); }
|
|
474
478
|
#mode-eggs { position: fixed; display: flex; flex-direction: row; align-items: center; gap: 4px; cursor: default; z-index: 2; pointer-events: none; transition: opacity 0.3s ease; }
|
|
475
479
|
body.fullscreen-mode .mode-badge { background-color: rgba(30, 30, 30, 0.75); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
|
|
@@ -618,6 +622,10 @@
|
|
|
618
622
|
background: var(--surface); border: 1px solid var(--border);
|
|
619
623
|
border-radius: var(--radius); pointer-events: none;
|
|
620
624
|
}
|
|
625
|
+
/* ── MIP 3-D volume renderer ─────────────────────────────── */
|
|
626
|
+
#mip-canvas { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; cursor: grab; z-index: 5; }
|
|
627
|
+
#mip-canvas:active { cursor: grabbing; }
|
|
628
|
+
|
|
621
629
|
#multi-view-wrap { display: none; width: 100%; height: 100%; }
|
|
622
630
|
#multi-view-wrap.active { display: flex; flex-direction: column; align-items: stretch; justify-content: center; gap: 12px; }
|
|
623
631
|
#mv-panes { display: flex; align-items: flex-start; justify-content: center; flex: 0 0 auto; gap: 16px; }
|
|
@@ -967,7 +975,9 @@
|
|
|
967
975
|
#colormap-strip, #colormap-strip-right {
|
|
968
976
|
position:fixed; display:flex; flex-direction:row; align-items:center; gap:8px;
|
|
969
977
|
opacity:0; pointer-events:none; transition:opacity 0.25s ease;
|
|
970
|
-
z-index:8;
|
|
978
|
+
z-index:8; overflow:hidden; justify-content:center;
|
|
979
|
+
mask-image:linear-gradient(to right, transparent, black 8%, black 92%, transparent);
|
|
980
|
+
-webkit-mask-image:linear-gradient(to right, transparent, black 8%, black 92%, transparent);
|
|
971
981
|
}
|
|
972
982
|
#colormap-strip.visible, #colormap-strip-right.visible { opacity:1; }
|
|
973
983
|
.cmap-thumb { display:flex; flex-direction:column; align-items:center; gap:2px; opacity:0.5; transition:opacity 0.15s ease; }
|
|
@@ -1366,6 +1376,7 @@
|
|
|
1366
1376
|
</div>
|
|
1367
1377
|
</div>
|
|
1368
1378
|
</div>
|
|
1379
|
+
<canvas id="mip-canvas"></canvas>
|
|
1369
1380
|
<div id="multi-view-wrap"></div>
|
|
1370
1381
|
<div id="qmri-view-wrap"></div>
|
|
1371
1382
|
<div id="loading-overlay"></div>
|
|
@@ -1473,7 +1484,7 @@
|
|
|
1473
1484
|
<div class="help-row"><span class="key">Shift+O</span><span class="desc">cycle overlay visibility: all → off → individual masks (when overlays active)</span></div>
|
|
1474
1485
|
<div class="help-row"><span class="key">X</span><span class="desc">cycle center pane: off → A−B → |A−B| → |A−B|/|A| → overlay → wipe → flicker → checker</span></div>
|
|
1475
1486
|
<div class="help-row"><span class="key">< / ></span><span class="desc">movie: fps (during playback)</span></div>
|
|
1476
|
-
<div class="help-row"><span class="key">[ / ]</span><span class="desc">flicker: rate · checker: tile size · overlay/wipe/reg blend · arrow density
|
|
1487
|
+
<div class="help-row"><span class="key">[ / ]</span><span class="desc">flicker: rate · checker: tile size · overlay/wipe/reg blend · arrow density</span></div>
|
|
1477
1488
|
<div class="help-row"><span class="key">{ / }</span><span class="desc">arrow length shorter / longer (vector field mode)</span></div>
|
|
1478
1489
|
<div class="help-row"><span class="key">U</span><span class="desc">toggle vector arrows</span></div>
|
|
1479
1490
|
<div class="help-row"><span class="key">n</span><span class="desc">cycle compare target session</span></div>
|
|
@@ -1482,7 +1493,7 @@
|
|
|
1482
1493
|
<div class="help-row"><span class="key">shift+drag</span><span class="desc">move dimbar / colorbar in immersive mode (resets on exit)</span></div>
|
|
1483
1494
|
<div class="help-row"><span class="key">Z</span><span class="desc">focus center pane (when compare center is active — X to activate)</span></div>
|
|
1484
1495
|
<div class="help-row"><span class="key">L</span><span class="desc">toggle log scale</span></div>
|
|
1485
|
-
<div class="help-row"><span class="key">p</span><span class="desc">cycle projection: off → MAX → MIN → MEAN → STD → SOS → SUM</span></div>
|
|
1496
|
+
<div class="help-row"><span class="key">p</span><span class="desc">cycle projection: off → MAX → MIN → MEAN → STD → SOS → SUM; in multiview: toggle 3-D MIP volume renderer</span></div>
|
|
1486
1497
|
<div class="help-row"><span class="key">m</span><span class="desc">cycle complex mode (mag / phase / real / imag)</span></div>
|
|
1487
1498
|
<div class="help-row"><span class="key">f</span><span class="desc">toggle centred FFT (prompts for axes)</span></div>
|
|
1488
1499
|
<div class="help-row"><span class="key">M</span><span class="desc">toggle alpha transparency</span></div>
|
|
@@ -1837,6 +1848,7 @@
|
|
|
1837
1848
|
const PROJECTION_COLORS = ['#4cc9f0', '#ff6b6b', '#80ed99', '#c77dff', '#ffa62b'];
|
|
1838
1849
|
let alphaLevel = 0; // 0=off, 1=on (transparent below vmin)
|
|
1839
1850
|
let multiViewActive = false;
|
|
1851
|
+
let mipActive = false;
|
|
1840
1852
|
let mvViews = [];
|
|
1841
1853
|
let mvDims = [];
|
|
1842
1854
|
let mvDraggingView = null;
|
|
@@ -2259,6 +2271,7 @@
|
|
|
2259
2271
|
function _cbApplyAndRender() {
|
|
2260
2272
|
proxyFetch(`/clearcache/${sid}`);
|
|
2261
2273
|
updateView();
|
|
2274
|
+
_mipOnWindowChange();
|
|
2262
2275
|
saveState();
|
|
2263
2276
|
}
|
|
2264
2277
|
// Legacy slimCbCanvas wheel/mousedown/dblclick handlers REMOVED (Task 3).
|
|
@@ -3340,6 +3353,7 @@
|
|
|
3340
3353
|
ModeRegistry.scaleAll();
|
|
3341
3354
|
positionEggs();
|
|
3342
3355
|
_validateUIState();
|
|
3356
|
+
if (mipActive) _mipResizeCanvas();
|
|
3343
3357
|
});
|
|
3344
3358
|
|
|
3345
3359
|
// ── Mode crossfade utility ──────────────────────────────
|
|
@@ -3443,7 +3457,6 @@
|
|
|
3443
3457
|
// ── Fullscreen overlay colorbar mode ──────────────────────
|
|
3444
3458
|
if (_fullscreenActive) {
|
|
3445
3459
|
// Fullscreen: use morph draw, then position anchored to bottom
|
|
3446
|
-
if (_cmapInIsland) return;
|
|
3447
3460
|
const cbRefW = compareActive ? window.innerWidth : (vpEl ? vpEl.offsetWidth : Math.round(canvasRect.width));
|
|
3448
3461
|
// Cap width: use pane viewport width if available, otherwise % of reference
|
|
3449
3462
|
let maxCbW = 350;
|
|
@@ -3494,8 +3507,6 @@
|
|
|
3494
3507
|
}
|
|
3495
3508
|
|
|
3496
3509
|
// ── Normal horizontal colorbar ──────────────────────────
|
|
3497
|
-
// Skip everything if colormap preview is showing in the island
|
|
3498
|
-
if (_cmapInIsland) return;
|
|
3499
3510
|
if (wrap) {
|
|
3500
3511
|
wrap.classList.remove('compact-vertical', 'compact-overlay');
|
|
3501
3512
|
wrap.style.transform = '';
|
|
@@ -3580,7 +3591,7 @@
|
|
|
3580
3591
|
// When collapsed: show window values (manualVmin/Vmax or currentVmin/Vmax)
|
|
3581
3592
|
const slimVmin = document.getElementById('slim-cb-vmin');
|
|
3582
3593
|
const slimVmax = document.getElementById('slim-cb-vmax');
|
|
3583
|
-
if (slimVmin && slimVmax
|
|
3594
|
+
if (slimVmin && slimVmax) {
|
|
3584
3595
|
if (compareActive) {
|
|
3585
3596
|
const sharedVmin = Math.min(...compareFrames.filter(Boolean).map(f => manualVmin ?? f.vmin));
|
|
3586
3597
|
const sharedVmax = Math.max(...compareFrames.filter(Boolean).map(f => manualVmax ?? f.vmax));
|
|
@@ -4058,7 +4069,7 @@
|
|
|
4058
4069
|
if (vminFrac > 0.005 || vmaxFrac < 0.995) {
|
|
4059
4070
|
[effVmin, effVmax].forEach(f => {
|
|
4060
4071
|
if (f <= 0.003 || f >= 0.997) return;
|
|
4061
|
-
const x = Math.round(f * cssW);
|
|
4072
|
+
const x = Math.max(2, Math.min(cssW - 2, Math.round(f * cssW)));
|
|
4062
4073
|
|
|
4063
4074
|
const lineH = cssH - labelH;
|
|
4064
4075
|
const lineTop = labelH;
|
|
@@ -4651,6 +4662,7 @@
|
|
|
4651
4662
|
_cbWindowChangeRafId = null;
|
|
4652
4663
|
updateView();
|
|
4653
4664
|
triggerPreload();
|
|
4665
|
+
_mipOnWindowChange();
|
|
4654
4666
|
saveState();
|
|
4655
4667
|
});
|
|
4656
4668
|
}
|
|
@@ -5919,6 +5931,9 @@
|
|
|
5919
5931
|
if (projectionMode > 0) {
|
|
5920
5932
|
html += `<span class="mode-badge mode-badge-proj">${PROJECTION_LABELS[projectionMode - 1]}</span>`;
|
|
5921
5933
|
}
|
|
5934
|
+
if (mipActive) {
|
|
5935
|
+
html += `<span class="mode-badge mode-badge-mip">MIP</span>`;
|
|
5936
|
+
}
|
|
5922
5937
|
el.innerHTML = html;
|
|
5923
5938
|
// Position eggs below the active canvas, just above the slim colorbar
|
|
5924
5939
|
if (html) positionEggs();
|
|
@@ -8317,6 +8332,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
8317
8332
|
return;
|
|
8318
8333
|
}
|
|
8319
8334
|
if (e.key === 'Escape') {
|
|
8335
|
+
if (mipActive) { exitMipMode(); return; }
|
|
8320
8336
|
helpOverlay.classList.remove('visible');
|
|
8321
8337
|
document.getElementById('info-overlay').classList.remove('visible');
|
|
8322
8338
|
return;
|
|
@@ -8508,13 +8524,6 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
8508
8524
|
return;
|
|
8509
8525
|
}
|
|
8510
8526
|
} else if (e.key === '[' || e.key === ']') {
|
|
8511
|
-
if (isPlaying) {
|
|
8512
|
-
const delta = e.key === ']' ? 5 : -5;
|
|
8513
|
-
playFps = Math.max(1, Math.min(120, playFps + delta));
|
|
8514
|
-
setStatus(`▶ playing (Space to stop · < / > fps: ${playFps})`);
|
|
8515
|
-
showToast(`playback: ${playFps} fps`);
|
|
8516
|
-
return;
|
|
8517
|
-
}
|
|
8518
8527
|
const step = e.key === ']' ? 0.05 : -0.05;
|
|
8519
8528
|
if (rectRoiMode && _roiShape === 'floodfill') {
|
|
8520
8529
|
// Adjust flood fill tolerance
|
|
@@ -8879,6 +8888,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
8879
8888
|
colormap_idx = (colormap_idx === -1 ? 0 : (colormap_idx + 1) % COLORMAPS.length);
|
|
8880
8889
|
proxyFetch(`/clearcache/${sid}`); updateView(); triggerPreload();
|
|
8881
8890
|
refreshAxesColor();
|
|
8891
|
+
_mipOnColormapChange();
|
|
8882
8892
|
// When histogram is open, skip the previewer — just update the colorbar display
|
|
8883
8893
|
const _histOpen = primaryCb._expanded || (_diffLeftCb && _diffLeftCb.expanded);
|
|
8884
8894
|
if (_histOpen) {
|
|
@@ -8909,6 +8919,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
8909
8919
|
colormap_idx = -1;
|
|
8910
8920
|
proxyFetch(`/clearcache/${sid}`); updateView(); triggerPreload();
|
|
8911
8921
|
refreshAxesColor();
|
|
8922
|
+
_mipOnColormapChange();
|
|
8912
8923
|
showStatus(`colormap: ${customColormap}`);
|
|
8913
8924
|
saveState();
|
|
8914
8925
|
});
|
|
@@ -9230,7 +9241,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
9230
9241
|
} else if (e.key === 'h' || e.key === 'ArrowLeft') {
|
|
9231
9242
|
e.preventDefault();
|
|
9232
9243
|
// While colormap strip is visible, cycle colormap backward
|
|
9233
|
-
if ((
|
|
9244
|
+
if ((_cmapStrip && _cmapStrip.classList.contains('visible')) && !qmriActive && !compareQmriActive && !rgbMode) {
|
|
9234
9245
|
if (_cmapStripMode === 'diff-center') {
|
|
9235
9246
|
const pool = (diffMode === 1) ? DIFF_COLORMAPS : ABS_DIFF_COLORMAPS;
|
|
9236
9247
|
const idx = pool.indexOf(_diffCenterColormap);
|
|
@@ -9277,7 +9288,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
9277
9288
|
} else if (e.key === 'l' || e.key === 'ArrowRight') {
|
|
9278
9289
|
e.preventDefault();
|
|
9279
9290
|
// While colormap strip is visible, cycle colormap forward
|
|
9280
|
-
if ((
|
|
9291
|
+
if ((_cmapStrip && _cmapStrip.classList.contains('visible')) && !qmriActive && !compareQmriActive && !rgbMode) {
|
|
9281
9292
|
if (_cmapStripMode === 'diff-center') {
|
|
9282
9293
|
const pool = (diffMode === 1) ? DIFF_COLORMAPS : ABS_DIFF_COLORMAPS;
|
|
9283
9294
|
const idx = pool.indexOf(_diffCenterColormap);
|
|
@@ -9343,6 +9354,12 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
9343
9354
|
renderEggs();
|
|
9344
9355
|
saveState();
|
|
9345
9356
|
} else if (e.key === 'p') {
|
|
9357
|
+
// In multiview: toggle 3-D MIP volume renderer
|
|
9358
|
+
if (multiViewActive || mipActive) {
|
|
9359
|
+
if (mipActive) { exitMipMode(); }
|
|
9360
|
+
else { enterMipMode(); }
|
|
9361
|
+
return;
|
|
9362
|
+
}
|
|
9346
9363
|
// Statistical projections: off → MAX → MIN → MEAN → STD → SOS
|
|
9347
9364
|
if (shape.length < 3) { showStatus('projection: need ≥ 3D array'); return; }
|
|
9348
9365
|
if (hasVectorfield) { showStatus('projection: not available in vector field mode'); return; }
|
|
@@ -10325,7 +10342,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
10325
10342
|
getWindow: () => ({ vmin: manualVmin ?? currentVmin, vmax: manualVmax ?? currentVmax }),
|
|
10326
10343
|
setWindow: (lo, hi) => { manualVmin = lo; manualVmax = hi; },
|
|
10327
10344
|
fetchHistogram: () => _fetchVolumeHistogram(_volHistSpatialOpts()),
|
|
10328
|
-
onWindowChange: () => { for (const v of mvViews) mvRender(v); },
|
|
10345
|
+
onWindowChange: () => { for (const v of mvViews) mvRender(v); _mipOnWindowChange(); },
|
|
10329
10346
|
onBinHover: (binIdx, frac) => {
|
|
10330
10347
|
if (!lebesgueMode) return;
|
|
10331
10348
|
if (binIdx < 0) { _clearLebesgueOverlay(); return; }
|
|
@@ -10503,6 +10520,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
10503
10520
|
}
|
|
10504
10521
|
|
|
10505
10522
|
function exitMultiView() {
|
|
10523
|
+
if (mipActive) exitMipMode();
|
|
10506
10524
|
multiViewActive = false;
|
|
10507
10525
|
// Clean up multiview vector field state
|
|
10508
10526
|
_mvVfieldCache.clear();
|
|
@@ -10539,6 +10557,483 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
10539
10557
|
showStatus('3-plane view: off');
|
|
10540
10558
|
}
|
|
10541
10559
|
|
|
10560
|
+
// ═══════════════════════════════════════════════════════════
|
|
10561
|
+
// ── 3-D Maximum Intensity Projection (MIP) renderer ──────
|
|
10562
|
+
// ═══════════════════════════════════════════════════════════
|
|
10563
|
+
|
|
10564
|
+
// WebGL2 state
|
|
10565
|
+
let _mipGL = null;
|
|
10566
|
+
let _mipProgram = null;
|
|
10567
|
+
let _mipVolTex = null;
|
|
10568
|
+
let _mipLutTex = null;
|
|
10569
|
+
let _mipVAO = null;
|
|
10570
|
+
let _mipAzimuth = 0;
|
|
10571
|
+
let _mipElevation = 0;
|
|
10572
|
+
let _mipZoom = 2.5;
|
|
10573
|
+
let _mipDragging = false;
|
|
10574
|
+
let _mipDragX = 0;
|
|
10575
|
+
let _mipDragY = 0;
|
|
10576
|
+
let _mipVolShape = [1, 1, 1];
|
|
10577
|
+
let _mipVolCacheKey = null; // "sid:dims:indices:version" to detect changes
|
|
10578
|
+
let _mipAnimFrame = null;
|
|
10579
|
+
|
|
10580
|
+
const _MIP_VERT_SRC = `#version 300 es
|
|
10581
|
+
in vec2 a_pos;
|
|
10582
|
+
out vec2 v_uv;
|
|
10583
|
+
void main() {
|
|
10584
|
+
v_uv = a_pos * 0.5 + 0.5;
|
|
10585
|
+
gl_Position = vec4(a_pos, 0.0, 1.0);
|
|
10586
|
+
}`;
|
|
10587
|
+
|
|
10588
|
+
const _MIP_FRAG_SRC = `#version 300 es
|
|
10589
|
+
precision highp float;
|
|
10590
|
+
precision highp sampler3D;
|
|
10591
|
+
in vec2 v_uv;
|
|
10592
|
+
out vec4 fragColor;
|
|
10593
|
+
|
|
10594
|
+
uniform sampler3D u_volume;
|
|
10595
|
+
uniform sampler2D u_lut;
|
|
10596
|
+
uniform vec3 u_volSize; // normalized size ratios
|
|
10597
|
+
uniform float u_vmin;
|
|
10598
|
+
uniform float u_vmax;
|
|
10599
|
+
uniform mat3 u_rotation;
|
|
10600
|
+
uniform float u_camDist;
|
|
10601
|
+
uniform int u_steps;
|
|
10602
|
+
|
|
10603
|
+
void main() {
|
|
10604
|
+
// Ray origin and direction from orbit camera with perspective
|
|
10605
|
+
vec3 center = vec3(0.0);
|
|
10606
|
+
vec3 camPos = u_rotation * vec3(0.0, 0.0, u_camDist);
|
|
10607
|
+
|
|
10608
|
+
// Build ray direction with slight perspective
|
|
10609
|
+
vec3 right = u_rotation * vec3(1.0, 0.0, 0.0);
|
|
10610
|
+
vec3 up = u_rotation * vec3(0.0, 1.0, 0.0);
|
|
10611
|
+
vec3 fwd = u_rotation * vec3(0.0, 0.0, -1.0);
|
|
10612
|
+
|
|
10613
|
+
float fov = 0.8; // radians
|
|
10614
|
+
vec2 ndc = (v_uv - 0.5) * 2.0;
|
|
10615
|
+
vec3 rayDir = normalize(fwd + ndc.x * right * fov + ndc.y * up * fov);
|
|
10616
|
+
vec3 rayOri = camPos;
|
|
10617
|
+
|
|
10618
|
+
// AABB intersection with box [-0.5*volSize, 0.5*volSize]
|
|
10619
|
+
vec3 halfBox = u_volSize * 0.5;
|
|
10620
|
+
vec3 invDir = 1.0 / rayDir;
|
|
10621
|
+
vec3 t0 = (-halfBox - rayOri) * invDir;
|
|
10622
|
+
vec3 t1 = ( halfBox - rayOri) * invDir;
|
|
10623
|
+
vec3 tmin = min(t0, t1);
|
|
10624
|
+
vec3 tmax = max(t0, t1);
|
|
10625
|
+
float tNear = max(max(tmin.x, tmin.y), tmin.z);
|
|
10626
|
+
float tFar = min(min(tmax.x, tmax.y), tmax.z);
|
|
10627
|
+
|
|
10628
|
+
if (tNear > tFar || tFar < 0.0) {
|
|
10629
|
+
fragColor = vec4(0.0, 0.0, 0.0, 1.0);
|
|
10630
|
+
return;
|
|
10631
|
+
}
|
|
10632
|
+
tNear = max(tNear, 0.0);
|
|
10633
|
+
|
|
10634
|
+
// Ray-march: find maximum intensity
|
|
10635
|
+
float maxVal = -1e30;
|
|
10636
|
+
float stepSize = (tFar - tNear) / float(u_steps);
|
|
10637
|
+
for (int i = 0; i < 512; i++) {
|
|
10638
|
+
if (i >= u_steps) break;
|
|
10639
|
+
float t = tNear + (float(i) + 0.5) * stepSize;
|
|
10640
|
+
vec3 pos = rayOri + t * rayDir;
|
|
10641
|
+
// Map to texture coords [0,1]
|
|
10642
|
+
vec3 tc = pos / u_volSize + 0.5;
|
|
10643
|
+
float val = texture(u_volume, tc).r;
|
|
10644
|
+
maxVal = max(maxVal, val);
|
|
10645
|
+
}
|
|
10646
|
+
|
|
10647
|
+
// Windowing
|
|
10648
|
+
float intensity = clamp((maxVal - u_vmin) / (u_vmax - u_vmin + 1e-10), 0.0, 1.0);
|
|
10649
|
+
|
|
10650
|
+
// Colormap lookup
|
|
10651
|
+
vec3 color = texture(u_lut, vec2(intensity, 0.5)).rgb;
|
|
10652
|
+
fragColor = vec4(color, 1.0);
|
|
10653
|
+
}`;
|
|
10654
|
+
|
|
10655
|
+
function _mipBuildRotationMatrix(az, el) {
|
|
10656
|
+
const ca = Math.cos(az), sa = Math.sin(az);
|
|
10657
|
+
const ce = Math.cos(el), se = Math.sin(el);
|
|
10658
|
+
// Rotation: first azimuth around Y, then elevation around X
|
|
10659
|
+
return [
|
|
10660
|
+
ca, sa * se, sa * ce,
|
|
10661
|
+
0, ce, -se,
|
|
10662
|
+
-sa, ca * se, ca * ce,
|
|
10663
|
+
];
|
|
10664
|
+
}
|
|
10665
|
+
|
|
10666
|
+
function _mipInitGL() {
|
|
10667
|
+
const canvas = document.getElementById('mip-canvas');
|
|
10668
|
+
if (_mipGL) return _mipGL;
|
|
10669
|
+
const gl = canvas.getContext('webgl2', { antialias: false, alpha: false });
|
|
10670
|
+
if (!gl) { showStatus('WebGL2 not available'); return null; }
|
|
10671
|
+
gl.getExtension('OES_texture_float_linear'); // required for LINEAR filtering on R32F textures
|
|
10672
|
+
_mipGL = gl;
|
|
10673
|
+
|
|
10674
|
+
// Compile shaders
|
|
10675
|
+
function compile(type, src) {
|
|
10676
|
+
const s = gl.createShader(type);
|
|
10677
|
+
gl.shaderSource(s, src);
|
|
10678
|
+
gl.compileShader(s);
|
|
10679
|
+
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
|
|
10680
|
+
console.error('MIP shader error:', gl.getShaderInfoLog(s));
|
|
10681
|
+
return null;
|
|
10682
|
+
}
|
|
10683
|
+
return s;
|
|
10684
|
+
}
|
|
10685
|
+
const vs = compile(gl.VERTEX_SHADER, _MIP_VERT_SRC);
|
|
10686
|
+
const fs = compile(gl.FRAGMENT_SHADER, _MIP_FRAG_SRC);
|
|
10687
|
+
const prog = gl.createProgram();
|
|
10688
|
+
gl.attachShader(prog, vs);
|
|
10689
|
+
gl.attachShader(prog, fs);
|
|
10690
|
+
gl.linkProgram(prog);
|
|
10691
|
+
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
|
10692
|
+
console.error('MIP program link error:', gl.getProgramInfoLog(prog));
|
|
10693
|
+
return null;
|
|
10694
|
+
}
|
|
10695
|
+
_mipProgram = prog;
|
|
10696
|
+
|
|
10697
|
+
// Full-screen quad VAO
|
|
10698
|
+
_mipVAO = gl.createVertexArray();
|
|
10699
|
+
gl.bindVertexArray(_mipVAO);
|
|
10700
|
+
const buf = gl.createBuffer();
|
|
10701
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
|
10702
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
|
10703
|
+
-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1
|
|
10704
|
+
]), gl.STATIC_DRAW);
|
|
10705
|
+
const loc = gl.getAttribLocation(prog, 'a_pos');
|
|
10706
|
+
gl.enableVertexAttribArray(loc);
|
|
10707
|
+
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
|
|
10708
|
+
gl.bindVertexArray(null);
|
|
10709
|
+
|
|
10710
|
+
// Create LUT texture
|
|
10711
|
+
_mipLutTex = gl.createTexture();
|
|
10712
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
10713
|
+
gl.bindTexture(gl.TEXTURE_2D, _mipLutTex);
|
|
10714
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
10715
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
10716
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
10717
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
10718
|
+
|
|
10719
|
+
return gl;
|
|
10720
|
+
}
|
|
10721
|
+
|
|
10722
|
+
function _mipBuildLUT() {
|
|
10723
|
+
const gl = _mipGL;
|
|
10724
|
+
if (!gl || !_mipLutTex) return;
|
|
10725
|
+
const stops = colormap_idx === -1 ? customGradientStops : COLORMAP_GRADIENT_STOPS[COLORMAPS[colormap_idx]];
|
|
10726
|
+
if (!stops || !stops.length) return;
|
|
10727
|
+
|
|
10728
|
+
// Build 256-wide RGBA LUT from gradient stops
|
|
10729
|
+
// Stops are [r, g, b] triplets evenly spaced across the colormap
|
|
10730
|
+
const lutSize = 256;
|
|
10731
|
+
const data = new Uint8Array(lutSize * 4);
|
|
10732
|
+
const n = stops.length;
|
|
10733
|
+
|
|
10734
|
+
for (let i = 0; i < lutSize; i++) {
|
|
10735
|
+
const t = i / (lutSize - 1) * (n - 1);
|
|
10736
|
+
const lo = Math.floor(t);
|
|
10737
|
+
const hi = Math.min(lo + 1, n - 1);
|
|
10738
|
+
const f = t - lo;
|
|
10739
|
+
data[i * 4 + 0] = Math.round(stops[lo][0] + (stops[hi][0] - stops[lo][0]) * f);
|
|
10740
|
+
data[i * 4 + 1] = Math.round(stops[lo][1] + (stops[hi][1] - stops[lo][1]) * f);
|
|
10741
|
+
data[i * 4 + 2] = Math.round(stops[lo][2] + (stops[hi][2] - stops[lo][2]) * f);
|
|
10742
|
+
data[i * 4 + 3] = 255;
|
|
10743
|
+
}
|
|
10744
|
+
|
|
10745
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
10746
|
+
gl.bindTexture(gl.TEXTURE_2D, _mipLutTex);
|
|
10747
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, lutSize, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
|
10748
|
+
}
|
|
10749
|
+
|
|
10750
|
+
function _mipUploadVolume(float32Data, shape) {
|
|
10751
|
+
const gl = _mipGL;
|
|
10752
|
+
if (!gl) return;
|
|
10753
|
+
if (_mipVolTex) gl.deleteTexture(_mipVolTex);
|
|
10754
|
+
_mipVolTex = gl.createTexture();
|
|
10755
|
+
_mipVolShape = shape;
|
|
10756
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
10757
|
+
gl.bindTexture(gl.TEXTURE_3D, _mipVolTex);
|
|
10758
|
+
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
10759
|
+
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
10760
|
+
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
10761
|
+
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
10762
|
+
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE);
|
|
10763
|
+
gl.texImage3D(gl.TEXTURE_3D, 0, gl.R32F,
|
|
10764
|
+
shape[2], shape[1], shape[0], // width=Z, height=Y, depth=X → texture(tc) where tc.xyz maps to [col, row, slice]
|
|
10765
|
+
0, gl.RED, gl.FLOAT, float32Data);
|
|
10766
|
+
}
|
|
10767
|
+
|
|
10768
|
+
function _mipRender() {
|
|
10769
|
+
const gl = _mipGL;
|
|
10770
|
+
if (!gl || !_mipProgram || !_mipVolTex) return;
|
|
10771
|
+
|
|
10772
|
+
const canvas = document.getElementById('mip-canvas');
|
|
10773
|
+
const dpr = window.devicePixelRatio || 1;
|
|
10774
|
+
const w = canvas.clientWidth;
|
|
10775
|
+
const h = canvas.clientHeight;
|
|
10776
|
+
if (canvas.width !== w * dpr || canvas.height !== h * dpr) {
|
|
10777
|
+
canvas.width = w * dpr;
|
|
10778
|
+
canvas.height = h * dpr;
|
|
10779
|
+
}
|
|
10780
|
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
10781
|
+
|
|
10782
|
+
gl.clearColor(0, 0, 0, 1);
|
|
10783
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
10784
|
+
|
|
10785
|
+
gl.useProgram(_mipProgram);
|
|
10786
|
+
|
|
10787
|
+
// Volume size: normalize so longest side = 1
|
|
10788
|
+
// Texture layout: width=shape[2], height=shape[1], depth=shape[0]
|
|
10789
|
+
// World-space axes: x=width(shape[2]), y=height(shape[1]), z=depth(shape[0])
|
|
10790
|
+
const s = _mipVolShape;
|
|
10791
|
+
const maxS = Math.max(s[0], s[1], s[2]);
|
|
10792
|
+
const volSize = [s[2] / maxS, s[1] / maxS, s[0] / maxS];
|
|
10793
|
+
|
|
10794
|
+
// Uniforms
|
|
10795
|
+
const rotMat = _mipBuildRotationMatrix(_mipAzimuth, _mipElevation);
|
|
10796
|
+
|
|
10797
|
+
gl.uniform3f(gl.getUniformLocation(_mipProgram, 'u_volSize'), volSize[0], volSize[1], volSize[2]);
|
|
10798
|
+
gl.uniform1f(gl.getUniformLocation(_mipProgram, 'u_vmin'), manualVmin ?? currentVmin);
|
|
10799
|
+
gl.uniform1f(gl.getUniformLocation(_mipProgram, 'u_vmax'), manualVmax ?? currentVmax);
|
|
10800
|
+
gl.uniformMatrix3fv(gl.getUniformLocation(_mipProgram, 'u_rotation'), false, rotMat);
|
|
10801
|
+
gl.uniform1f(gl.getUniformLocation(_mipProgram, 'u_camDist'), _mipZoom);
|
|
10802
|
+
gl.uniform1i(gl.getUniformLocation(_mipProgram, 'u_steps'), 256);
|
|
10803
|
+
|
|
10804
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
10805
|
+
gl.bindTexture(gl.TEXTURE_3D, _mipVolTex);
|
|
10806
|
+
gl.uniform1i(gl.getUniformLocation(_mipProgram, 'u_volume'), 0);
|
|
10807
|
+
|
|
10808
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
10809
|
+
gl.bindTexture(gl.TEXTURE_2D, _mipLutTex);
|
|
10810
|
+
gl.uniform1i(gl.getUniformLocation(_mipProgram, 'u_lut'), 1);
|
|
10811
|
+
|
|
10812
|
+
gl.bindVertexArray(_mipVAO);
|
|
10813
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
10814
|
+
gl.bindVertexArray(null);
|
|
10815
|
+
}
|
|
10816
|
+
|
|
10817
|
+
function _mipResizeCanvas() {
|
|
10818
|
+
const canvas = document.getElementById('mip-canvas');
|
|
10819
|
+
const row = document.getElementById('viewer-row');
|
|
10820
|
+
const w = Math.max(100, window.innerWidth - 80);
|
|
10821
|
+
const h = Math.max(100, window.innerHeight - uiReserveV() - 80);
|
|
10822
|
+
// Set viewer-row to contain the canvas (it collapses when mv-wrap is hidden)
|
|
10823
|
+
row.style.minHeight = h + 'px';
|
|
10824
|
+
canvas.style.width = w + 'px';
|
|
10825
|
+
canvas.style.height = h + 'px';
|
|
10826
|
+
_mipRender();
|
|
10827
|
+
}
|
|
10828
|
+
|
|
10829
|
+
function _mipCacheKey() {
|
|
10830
|
+
const dims = mvDims.join(',');
|
|
10831
|
+
const extraIndices = indices.map((v, i) => mvDims.includes(i) ? '' : v).join(',');
|
|
10832
|
+
return `${sid}:${dims}:${extraIndices}:${_watchDataVersion}`;
|
|
10833
|
+
}
|
|
10834
|
+
|
|
10835
|
+
async function enterMipMode() {
|
|
10836
|
+
if (mipActive) return;
|
|
10837
|
+
if (!multiViewActive) { showStatus('MIP: enter multiview first (v)'); return; }
|
|
10838
|
+
if (shape.length < 3) { showStatus('MIP: need at least 3D array'); return; }
|
|
10839
|
+
|
|
10840
|
+
mipActive = true;
|
|
10841
|
+
_eggsVisible = true;
|
|
10842
|
+
|
|
10843
|
+
// Hide multiview panes, show MIP canvas
|
|
10844
|
+
const wrap = document.getElementById('multi-view-wrap');
|
|
10845
|
+
wrap.style.display = 'none';
|
|
10846
|
+
const canvas = document.getElementById('mip-canvas');
|
|
10847
|
+
canvas.style.display = 'block';
|
|
10848
|
+
|
|
10849
|
+
// Show shared colorbar for MIP mode (mv-cb is inside hidden wrap)
|
|
10850
|
+
_reconcileCbVisibility();
|
|
10851
|
+
drawSlimColorbar(cbMarkerFrac);
|
|
10852
|
+
|
|
10853
|
+
showStatus('Loading volume...');
|
|
10854
|
+
renderEggs();
|
|
10855
|
+
|
|
10856
|
+
// Init WebGL
|
|
10857
|
+
const gl = _mipInitGL();
|
|
10858
|
+
if (!gl) { exitMipMode(); return; }
|
|
10859
|
+
|
|
10860
|
+
// Fetch volume data
|
|
10861
|
+
const cacheKey = _mipCacheKey();
|
|
10862
|
+
if (_mipVolCacheKey !== cacheKey || !_mipVolTex) {
|
|
10863
|
+
try {
|
|
10864
|
+
const dims = mvDims.join(',');
|
|
10865
|
+
const idxStr = indices.join(',');
|
|
10866
|
+
const resp = await proxyFetch(`/volume_data/${sid}?dims=${dims}&indices=${idxStr}&complex_mode=${complexMode}`);
|
|
10867
|
+
if (!resp.ok) { showStatus('MIP: failed to load volume'); exitMipMode(); return; }
|
|
10868
|
+
const shapeStr = resp.headers.get('X-Shape');
|
|
10869
|
+
const vmin = parseFloat(resp.headers.get('X-Vmin'));
|
|
10870
|
+
const vmax = parseFloat(resp.headers.get('X-Vmax'));
|
|
10871
|
+
const volShape = shapeStr.split(',').map(Number);
|
|
10872
|
+
const buf = await resp.arrayBuffer();
|
|
10873
|
+
const f32 = new Float32Array(buf);
|
|
10874
|
+
|
|
10875
|
+
_mipUploadVolume(f32, volShape);
|
|
10876
|
+
_mipVolCacheKey = cacheKey;
|
|
10877
|
+
|
|
10878
|
+
// Set auto vmin/vmax if not manually overridden
|
|
10879
|
+
if (manualVmin === null) currentVmin = vmin;
|
|
10880
|
+
if (manualVmax === null) currentVmax = vmax;
|
|
10881
|
+
} catch (err) {
|
|
10882
|
+
console.error('MIP volume fetch error:', err);
|
|
10883
|
+
showStatus('MIP: volume load error');
|
|
10884
|
+
exitMipMode();
|
|
10885
|
+
return;
|
|
10886
|
+
}
|
|
10887
|
+
}
|
|
10888
|
+
|
|
10889
|
+
_mipBuildLUT();
|
|
10890
|
+
_mipResizeCanvas();
|
|
10891
|
+
|
|
10892
|
+
// Attach mouse handlers
|
|
10893
|
+
_mipAttachEvents();
|
|
10894
|
+
|
|
10895
|
+
showStatus('MIP — drag to rotate, scroll to zoom, p/Esc to exit', 3000);
|
|
10896
|
+
}
|
|
10897
|
+
|
|
10898
|
+
function exitMipMode() {
|
|
10899
|
+
if (!mipActive) return;
|
|
10900
|
+
mipActive = false;
|
|
10901
|
+
|
|
10902
|
+
const canvas = document.getElementById('mip-canvas');
|
|
10903
|
+
canvas.style.display = 'none';
|
|
10904
|
+
_mipDetachEvents();
|
|
10905
|
+
|
|
10906
|
+
if (_mipAnimFrame) { cancelAnimationFrame(_mipAnimFrame); _mipAnimFrame = null; }
|
|
10907
|
+
|
|
10908
|
+
// Show multiview again
|
|
10909
|
+
const wrap = document.getElementById('multi-view-wrap');
|
|
10910
|
+
wrap.style.display = '';
|
|
10911
|
+
document.getElementById('viewer-row').style.minHeight = '';
|
|
10912
|
+
|
|
10913
|
+
// Hide shared cb, restore mv colorbar
|
|
10914
|
+
_reconcileCbVisibility();
|
|
10915
|
+
|
|
10916
|
+
_eggsVisible = true;
|
|
10917
|
+
renderEggs();
|
|
10918
|
+
|
|
10919
|
+
// Re-render multiview panes
|
|
10920
|
+
for (const v of mvViews) mvRender(v);
|
|
10921
|
+
if (window._mvColorBar) window._mvColorBar.draw();
|
|
10922
|
+
|
|
10923
|
+
showStatus('MIP: off');
|
|
10924
|
+
}
|
|
10925
|
+
|
|
10926
|
+
// Mouse interaction
|
|
10927
|
+
let _mipEventsBound = false;
|
|
10928
|
+
function _mipAttachEvents() {
|
|
10929
|
+
if (_mipEventsBound) return;
|
|
10930
|
+
const canvas = document.getElementById('mip-canvas');
|
|
10931
|
+
canvas.addEventListener('mousedown', _mipOnMouseDown);
|
|
10932
|
+
canvas.addEventListener('mousemove', _mipOnMouseMove);
|
|
10933
|
+
canvas.addEventListener('mouseup', _mipOnMouseUp);
|
|
10934
|
+
canvas.addEventListener('mouseleave', _mipOnMouseUp);
|
|
10935
|
+
canvas.addEventListener('wheel', _mipOnWheel, { passive: false });
|
|
10936
|
+
canvas.addEventListener('contextmenu', _mipPreventDefault);
|
|
10937
|
+
// Touch support
|
|
10938
|
+
canvas.addEventListener('touchstart', _mipOnTouchStart, { passive: false });
|
|
10939
|
+
canvas.addEventListener('touchmove', _mipOnTouchMove, { passive: false });
|
|
10940
|
+
canvas.addEventListener('touchend', _mipOnTouchEnd);
|
|
10941
|
+
_mipEventsBound = true;
|
|
10942
|
+
}
|
|
10943
|
+
function _mipDetachEvents() {
|
|
10944
|
+
if (!_mipEventsBound) return;
|
|
10945
|
+
const canvas = document.getElementById('mip-canvas');
|
|
10946
|
+
canvas.removeEventListener('mousedown', _mipOnMouseDown);
|
|
10947
|
+
canvas.removeEventListener('mousemove', _mipOnMouseMove);
|
|
10948
|
+
canvas.removeEventListener('mouseup', _mipOnMouseUp);
|
|
10949
|
+
canvas.removeEventListener('mouseleave', _mipOnMouseUp);
|
|
10950
|
+
canvas.removeEventListener('wheel', _mipOnWheel);
|
|
10951
|
+
canvas.removeEventListener('contextmenu', _mipPreventDefault);
|
|
10952
|
+
canvas.removeEventListener('touchstart', _mipOnTouchStart);
|
|
10953
|
+
canvas.removeEventListener('touchmove', _mipOnTouchMove);
|
|
10954
|
+
canvas.removeEventListener('touchend', _mipOnTouchEnd);
|
|
10955
|
+
_mipEventsBound = false;
|
|
10956
|
+
}
|
|
10957
|
+
function _mipPreventDefault(e) { e.preventDefault(); e.stopPropagation(); }
|
|
10958
|
+
function _mipOnMouseDown(e) {
|
|
10959
|
+
e.preventDefault(); e.stopPropagation();
|
|
10960
|
+
_mipDragging = true;
|
|
10961
|
+
_mipDragX = e.clientX;
|
|
10962
|
+
_mipDragY = e.clientY;
|
|
10963
|
+
}
|
|
10964
|
+
function _mipOnMouseMove(e) {
|
|
10965
|
+
if (!_mipDragging) return;
|
|
10966
|
+
e.preventDefault(); e.stopPropagation();
|
|
10967
|
+
const dx = e.clientX - _mipDragX;
|
|
10968
|
+
const dy = e.clientY - _mipDragY;
|
|
10969
|
+
_mipDragX = e.clientX;
|
|
10970
|
+
_mipDragY = e.clientY;
|
|
10971
|
+
_mipAzimuth += dx * 0.008;
|
|
10972
|
+
_mipElevation = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, _mipElevation + dy * 0.008));
|
|
10973
|
+
_mipRender();
|
|
10974
|
+
}
|
|
10975
|
+
function _mipOnMouseUp(e) {
|
|
10976
|
+
_mipDragging = false;
|
|
10977
|
+
}
|
|
10978
|
+
function _mipOnWheel(e) {
|
|
10979
|
+
e.preventDefault(); e.stopPropagation();
|
|
10980
|
+
_mipZoom = Math.max(0.5, Math.min(10.0, _mipZoom + e.deltaY * 0.005));
|
|
10981
|
+
_mipRender();
|
|
10982
|
+
}
|
|
10983
|
+
// Touch events
|
|
10984
|
+
let _mipTouchPrev = null;
|
|
10985
|
+
let _mipTouchDist = null;
|
|
10986
|
+
function _mipOnTouchStart(e) {
|
|
10987
|
+
e.preventDefault(); e.stopPropagation();
|
|
10988
|
+
if (e.touches.length === 1) {
|
|
10989
|
+
_mipTouchPrev = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
10990
|
+
} else if (e.touches.length === 2) {
|
|
10991
|
+
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
10992
|
+
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
10993
|
+
_mipTouchDist = Math.sqrt(dx * dx + dy * dy);
|
|
10994
|
+
}
|
|
10995
|
+
}
|
|
10996
|
+
function _mipOnTouchMove(e) {
|
|
10997
|
+
e.preventDefault(); e.stopPropagation();
|
|
10998
|
+
if (e.touches.length === 1 && _mipTouchPrev) {
|
|
10999
|
+
const dx = e.touches[0].clientX - _mipTouchPrev.x;
|
|
11000
|
+
const dy = e.touches[0].clientY - _mipTouchPrev.y;
|
|
11001
|
+
_mipTouchPrev = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
11002
|
+
_mipAzimuth += dx * 0.008;
|
|
11003
|
+
_mipElevation = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, _mipElevation + dy * 0.008));
|
|
11004
|
+
_mipRender();
|
|
11005
|
+
} else if (e.touches.length === 2 && _mipTouchDist !== null) {
|
|
11006
|
+
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
11007
|
+
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
11008
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
11009
|
+
const delta = _mipTouchDist - dist;
|
|
11010
|
+
_mipZoom = Math.max(0.5, Math.min(10.0, _mipZoom + delta * 0.01));
|
|
11011
|
+
_mipTouchDist = dist;
|
|
11012
|
+
_mipRender();
|
|
11013
|
+
}
|
|
11014
|
+
}
|
|
11015
|
+
function _mipOnTouchEnd(e) {
|
|
11016
|
+
_mipTouchPrev = null;
|
|
11017
|
+
_mipTouchDist = null;
|
|
11018
|
+
}
|
|
11019
|
+
|
|
11020
|
+
// Called when colormap changes to update the LUT texture
|
|
11021
|
+
function _mipOnColormapChange() {
|
|
11022
|
+
if (!mipActive || !_mipGL) return;
|
|
11023
|
+
_mipBuildLUT();
|
|
11024
|
+
_mipRender();
|
|
11025
|
+
}
|
|
11026
|
+
|
|
11027
|
+
// Called when vmin/vmax changes to re-render
|
|
11028
|
+
function _mipOnWindowChange() {
|
|
11029
|
+
if (!mipActive || !_mipGL) return;
|
|
11030
|
+
_mipRender();
|
|
11031
|
+
}
|
|
11032
|
+
|
|
11033
|
+
// ═══════════════════════════════════════════════════════════
|
|
11034
|
+
// ── End MIP renderer ─────────────────────────────────────
|
|
11035
|
+
// ═══════════════════════════════════════════════════════════
|
|
11036
|
+
|
|
10542
11037
|
// --------------- qMRI mode ---------------
|
|
10543
11038
|
function enterQmri() {
|
|
10544
11039
|
// Auto-detect: if exactly one dim has size 3–6, use it; otherwise fall back to activeDim
|
|
@@ -13631,7 +14126,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
13631
14126
|
|
|
13632
14127
|
// 1. Shared colorbar
|
|
13633
14128
|
_sharedCbVisible = colorbarVisible() && !isCenter
|
|
13634
|
-
&& !multiViewActive && !compareMvActive
|
|
14129
|
+
&& (!multiViewActive || mipActive) && !compareMvActive
|
|
13635
14130
|
&& !qmriActive && !compareQmriActive;
|
|
13636
14131
|
const cbWrap = document.getElementById('slim-cb-wrap');
|
|
13637
14132
|
if (cbWrap) cbWrap.style.display = _sharedCbVisible ? '' : 'none';
|
|
@@ -13667,25 +14162,6 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
13667
14162
|
const _cmapStripRight = document.getElementById('colormap-strip-right');
|
|
13668
14163
|
let _cmapStripTimer = null;
|
|
13669
14164
|
let _cmapStripMode = 'normal'; // 'normal' | 'diff-center' | 'diff-side'
|
|
13670
|
-
let _cmapInIsland = false; // true when preview is rendered inside the island
|
|
13671
|
-
function _cmapIslandRestore() {
|
|
13672
|
-
if (!_cmapInIsland) return;
|
|
13673
|
-
_cmapInIsland = false;
|
|
13674
|
-
const inlinePreview = document.getElementById('cmap-island-preview');
|
|
13675
|
-
if (inlinePreview) inlinePreview.remove();
|
|
13676
|
-
const cv = document.getElementById('slim-cb');
|
|
13677
|
-
const triZone = document.getElementById('slim-cb-tri-zone');
|
|
13678
|
-
const vminEl = document.getElementById('slim-cb-vmin');
|
|
13679
|
-
const vmaxEl = document.getElementById('slim-cb-vmax');
|
|
13680
|
-
if (cv) cv.style.display = '';
|
|
13681
|
-
if (triZone) triZone.style.display = '';
|
|
13682
|
-
// Only show flanking values if histogram is collapsed
|
|
13683
|
-
if (vminEl) vminEl.style.display = primaryCb._expanded ? 'none' : '';
|
|
13684
|
-
if (vmaxEl) vmaxEl.style.display = primaryCb._expanded ? 'none' : '';
|
|
13685
|
-
// Redraw to restore correct state
|
|
13686
|
-
_syncPrimaryCb();
|
|
13687
|
-
primaryCb.draw();
|
|
13688
|
-
}
|
|
13689
14165
|
// anchorEl: optional DOM element to position the strip on (instead of default colorbar)
|
|
13690
14166
|
// extraFadeEls: optional array of extra DOM elements to fade out alongside the anchor
|
|
13691
14167
|
/* ── JS: Colormap Strip and Wipe/Flicker Compare Tools ───── */
|
|
@@ -13694,21 +14170,10 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
13694
14170
|
if (!_cmapStrip || !COLORMAP_GRADIENT_STOPS) return;
|
|
13695
14171
|
const pool = overridePool || COLORMAPS;
|
|
13696
14172
|
const ci = overrideName ? pool.indexOf(overrideName) : (colormap_idx === -1 ? 0 : colormap_idx);
|
|
13697
|
-
// Compute how many thumbs fit
|
|
14173
|
+
// Compute how many thumbs fit — never wider than the colorbar
|
|
13698
14174
|
const THUMB_W = 76;
|
|
13699
|
-
|
|
13700
|
-
const
|
|
13701
|
-
if (useIslandEarly) {
|
|
13702
|
-
const col = document.getElementById('slim-cb-col');
|
|
13703
|
-
if (col) availW = col.offsetWidth;
|
|
13704
|
-
} else if (_fullscreenActive && compareActive) {
|
|
13705
|
-
const firstInner = anchorEl ? anchorEl.closest('.compare-canvas-inner') : document.querySelector('.compare-canvas-inner');
|
|
13706
|
-
const vpW = firstInner && firstInner.dataset.vpW ? parseInt(firstInner.dataset.vpW) : 0;
|
|
13707
|
-
if (vpW > 0) availW = vpW;
|
|
13708
|
-
} else if (_fullscreenActive) {
|
|
13709
|
-
const vpEl = document.getElementById('canvas-viewport');
|
|
13710
|
-
if (vpEl) availW = vpEl.offsetWidth;
|
|
13711
|
-
}
|
|
14175
|
+
const cbWrap = anchorEl || document.getElementById(multiViewActive ? 'mv-cb-wrap' : 'slim-cb-wrap');
|
|
14176
|
+
const availW = cbWrap ? cbWrap.getBoundingClientRect().width : 999;
|
|
13712
14177
|
const rawThumbs = Math.max(1, Math.floor(availW / THUMB_W));
|
|
13713
14178
|
const maxThumbs = rawThumbs % 2 === 0 ? rawThumbs - 1 : rawThumbs;
|
|
13714
14179
|
const SIDE = Math.floor((maxThumbs - 1) / 2);
|
|
@@ -13743,37 +14208,7 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
13743
14208
|
_cmapStrip.appendChild(_makeThumb(ci + offs, offs === 0));
|
|
13744
14209
|
}
|
|
13745
14210
|
|
|
13746
|
-
// ---
|
|
13747
|
-
const useIsland = !anchorEl && !multiViewActive;
|
|
13748
|
-
if (useIsland) {
|
|
13749
|
-
_cmapIslandRestore(); // clean up any previous inline preview
|
|
13750
|
-
const col = document.getElementById('slim-cb-col');
|
|
13751
|
-
const cv = document.getElementById('slim-cb');
|
|
13752
|
-
const triZone = document.getElementById('slim-cb-tri-zone');
|
|
13753
|
-
const vminEl = document.getElementById('slim-cb-vmin');
|
|
13754
|
-
const vmaxEl = document.getElementById('slim-cb-vmax');
|
|
13755
|
-
// Hide normal colorbar contents
|
|
13756
|
-
if (cv) cv.style.display = 'none';
|
|
13757
|
-
if (triZone) triZone.style.display = 'none';
|
|
13758
|
-
if (vminEl) vminEl.style.display = 'none';
|
|
13759
|
-
if (vmaxEl) vmaxEl.style.display = 'none';
|
|
13760
|
-
// Create inline preview container and move strip content into it
|
|
13761
|
-
const preview = document.createElement('div');
|
|
13762
|
-
preview.id = 'cmap-island-preview';
|
|
13763
|
-
preview.style.cssText = 'display:flex;flex-direction:row;align-items:center;justify-content:center;gap:8px;width:100%;flex:1;overflow:hidden;mask-image:linear-gradient(to right, transparent, black 15%, black 85%, transparent);-webkit-mask-image:linear-gradient(to right, transparent, black 15%, black 85%, transparent);';
|
|
13764
|
-
while (_cmapStrip.firstChild) {
|
|
13765
|
-
preview.appendChild(_cmapStrip.firstChild);
|
|
13766
|
-
}
|
|
13767
|
-
col.appendChild(preview);
|
|
13768
|
-
_cmapInIsland = true;
|
|
13769
|
-
clearTimeout(_cmapStripTimer);
|
|
13770
|
-
_cmapStripTimer = setTimeout(() => {
|
|
13771
|
-
_cmapIslandRestore();
|
|
13772
|
-
}, 1500);
|
|
13773
|
-
return;
|
|
13774
|
-
}
|
|
13775
|
-
|
|
13776
|
-
// --- Anchored / multiview modes: floating strip (original behavior) ---
|
|
14211
|
+
// --- Floating strip positioned on the colorbar (or custom anchor) ---
|
|
13777
14212
|
const hasRightAnchor = extraFadeEls && extraFadeEls.length > 0 && extraFadeEls[0] && _cmapStripRight;
|
|
13778
14213
|
if (hasRightAnchor) {
|
|
13779
14214
|
for (let offs = -SIDE; offs <= SIDE; offs++) {
|
|
@@ -13781,21 +14216,22 @@ h1{font-size:22px;border-bottom:1px solid #2c2c2c;padding-bottom:12px;}</style><
|
|
|
13781
14216
|
}
|
|
13782
14217
|
}
|
|
13783
14218
|
// Position strip centered on the colorbar (or custom anchor)
|
|
13784
|
-
const cbWrap = anchorEl || document.getElementById(multiViewActive ? 'mv-cb-wrap' : 'slim-cb-wrap');
|
|
13785
14219
|
if (cbWrap) {
|
|
13786
14220
|
const cbRect = cbWrap.getBoundingClientRect();
|
|
13787
|
-
_cmapStrip.style.top = (cbRect.top + cbRect.height / 2
|
|
14221
|
+
_cmapStrip.style.top = (cbRect.top + (cbRect.height - 32) / 2) + 'px';
|
|
13788
14222
|
_cmapStrip.style.left = (cbRect.left + cbRect.width / 2) + 'px';
|
|
13789
14223
|
_cmapStrip.style.transform = 'translateX(-50%)';
|
|
14224
|
+
_cmapStrip.style.maxWidth = cbRect.width + 'px';
|
|
13790
14225
|
cbWrap.style.opacity = '0';
|
|
13791
14226
|
}
|
|
13792
14227
|
// Position right strip on the right pane's colorbar
|
|
13793
14228
|
if (hasRightAnchor) {
|
|
13794
14229
|
const rightAnchor = extraFadeEls[0];
|
|
13795
14230
|
const rRect = rightAnchor.getBoundingClientRect();
|
|
13796
|
-
_cmapStripRight.style.top = (rRect.top + rRect.height / 2
|
|
14231
|
+
_cmapStripRight.style.top = (rRect.top + (rRect.height - 32) / 2) + 'px';
|
|
13797
14232
|
_cmapStripRight.style.left = (rRect.left + rRect.width / 2) + 'px';
|
|
13798
14233
|
_cmapStripRight.style.transform = 'translateX(-50%)';
|
|
14234
|
+
_cmapStripRight.style.maxWidth = rRect.width + 'px';
|
|
13799
14235
|
rightAnchor.style.opacity = '0';
|
|
13800
14236
|
for (let i = 1; i < extraFadeEls.length; i++) {
|
|
13801
14237
|
if (extraFadeEls[i]) extraFadeEls[i].style.opacity = '0';
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|