arrayview 0.49.1__tar.gz → 0.50.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.49.1 → arrayview-0.50.0}/PKG-INFO +1 -1
- {arrayview-0.49.1 → arrayview-0.50.0}/pyproject.toml +1 -1
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_routes_websocket.py +64 -11
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_viewer.html +281 -31
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_browser.py +5 -3
- {arrayview-0.49.1 → arrayview-0.50.0}/.gitignore +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/LICENSE +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/README.md +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/__init__.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/__main__.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_analysis.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_app.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_bootstrap_app.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_config.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_diagnostics.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_dicom.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_diff.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_extra_ports.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_icon.png +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_imaging.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_instance_registry.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_io.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_launch_plan.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_launch_trace.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_launcher.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_lifecycle.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_overlays.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_platform.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_render.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_routes_analysis.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_routes_drop.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_routes_export.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_routes_loading.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_routes_persistence.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_routes_preferences.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_routes_preload.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_routes_query.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_routes_rendering.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_routes_segmentation.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_routes_state.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_routes_vectorfield.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_server.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_session.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_session_spec.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_shell.html +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_source_safety.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_synthetic_mri.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_torch.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_tutorial.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_vectorfield.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_view_memory.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_vscode.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_vscode_browser.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_vscode_extension.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_vscode_signal.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/_watch.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/gsap.min.js +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/src/arrayview/todo.md +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/capture_patient_loading.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/capture_v_animation.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/conftest.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_1d_arrays.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_api.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_backend_shared.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_cli.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_config.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_dicom.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_drop_import.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_instance_registry.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_io.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_large_arrays.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_launch_blackbox.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_nifti_series.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_safe_server_stop.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_source_safety.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_torch.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_tutorial.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_view_memory.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/test_vsix_bundle.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/ui_audit.py +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/v_anim_frames/.gitkeep +0 -0
- {arrayview-0.49.1 → arrayview-0.50.0}/tests/visual_smoke.py +0 -0
|
@@ -103,12 +103,22 @@ def _cancel_pending_viewer_release(sid: str) -> None:
|
|
|
103
103
|
task.cancel()
|
|
104
104
|
|
|
105
105
|
|
|
106
|
-
def _trace_viewer_event(event: str, sid: str) -> None:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
def _trace_viewer_event(event: str, sid: str, **attrs: object) -> None:
|
|
107
|
+
# Same opt-in as the page-route events (env or ~/.arrayview marker), so a
|
|
108
|
+
# server started by an Explorer click logs its sockets too: a launch that
|
|
109
|
+
# loads its page but never shows a frame needs both halves to diagnose.
|
|
110
|
+
try:
|
|
111
|
+
from arrayview._launch_trace import (
|
|
112
|
+
configure_route_launch_trace,
|
|
113
|
+
emit_launch_event,
|
|
114
|
+
trace_tag,
|
|
115
|
+
)
|
|
110
116
|
|
|
111
|
-
|
|
117
|
+
if not configure_route_launch_trace():
|
|
118
|
+
return
|
|
119
|
+
emit_launch_event(event, sid_tag=trace_tag(sid), **attrs)
|
|
120
|
+
except Exception:
|
|
121
|
+
return
|
|
112
122
|
|
|
113
123
|
def _render_progress_reporter(
|
|
114
124
|
loop, ws, label="Loading", quiet_for_s=0.6, min_interval_s=0.2
|
|
@@ -469,11 +479,21 @@ def register_websocket_routes(app) -> None:
|
|
|
469
479
|
# stops that clock. The viewer keeps its splash up until the metadata
|
|
470
480
|
# below arrives, so an early accept shows nothing half-rendered.
|
|
471
481
|
await ws.accept()
|
|
482
|
+
accepted_at = time.perf_counter()
|
|
483
|
+
arrived_on = (ws.scope.get("server") or (None, None))[1]
|
|
484
|
+
_trace_viewer_event(
|
|
485
|
+
"viewer.socket_accepted", sid,
|
|
486
|
+
request_id=launch_request_id, port=arrived_on,
|
|
487
|
+
session_ready=sid in _session_mod.SESSIONS,
|
|
488
|
+
)
|
|
472
489
|
|
|
473
490
|
session = _session_mod.SESSIONS.get(sid)
|
|
474
491
|
if session is None:
|
|
475
492
|
session = await _await_session_reporting_read_progress(ws, sid)
|
|
476
493
|
if not session:
|
|
494
|
+
_trace_viewer_event(
|
|
495
|
+
"viewer.session_missing", sid, request_id=launch_request_id
|
|
496
|
+
)
|
|
477
497
|
# The socket has already opened, and the viewer treats a drop after
|
|
478
498
|
# a successful open as a recoverable reconnect — it would retry
|
|
479
499
|
# forever. Say the load failed so it shows an error instead.
|
|
@@ -497,6 +517,11 @@ def register_websocket_routes(app) -> None:
|
|
|
497
517
|
or launch_journal is None
|
|
498
518
|
or launch_journal.get("token") != launch_token
|
|
499
519
|
):
|
|
520
|
+
_trace_viewer_event(
|
|
521
|
+
"viewer.launch_rejected", sid,
|
|
522
|
+
request_id=launch_request_id,
|
|
523
|
+
journal_found=launch_journal is not None,
|
|
524
|
+
)
|
|
500
525
|
await ws.close(code=1008)
|
|
501
526
|
return
|
|
502
527
|
|
|
@@ -525,10 +550,9 @@ def register_websocket_routes(app) -> None:
|
|
|
525
550
|
_session_mod.VIEWER_CONNECTIONS_SEEN += 1
|
|
526
551
|
_session_mod.LIVE_VIEWER_SOCKETS.append(ws)
|
|
527
552
|
_ensure_path_nudge_task()
|
|
528
|
-
#
|
|
529
|
-
# released the moment its own viewer goes away
|
|
530
|
-
# every viewer everywhere to close.
|
|
531
|
-
arrived_on = (ws.scope.get("server") or (None, None))[1]
|
|
553
|
+
# arrived_on (above) is the port this viewer came in on, so a
|
|
554
|
+
# cold-start port can be released the moment its own viewer goes away
|
|
555
|
+
# rather than waiting for every viewer everywhere to close.
|
|
532
556
|
try:
|
|
533
557
|
from arrayview import _extra_ports
|
|
534
558
|
|
|
@@ -544,7 +568,11 @@ def register_websocket_routes(app) -> None:
|
|
|
544
568
|
_session_mod.VIEWER_REQUEST_COUNTS[request_key] = (
|
|
545
569
|
_session_mod.VIEWER_REQUEST_COUNTS.get(request_key, 0) + 1
|
|
546
570
|
)
|
|
547
|
-
_trace_viewer_event(
|
|
571
|
+
_trace_viewer_event(
|
|
572
|
+
"viewer.connected", sid,
|
|
573
|
+
request_id=launch_request_id,
|
|
574
|
+
wait_ms=round((time.perf_counter() - accepted_at) * 1000),
|
|
575
|
+
)
|
|
548
576
|
loop = asyncio.get_running_loop()
|
|
549
577
|
_pending: asyncio.Queue[dict | None] = asyncio.Queue()
|
|
550
578
|
|
|
@@ -572,11 +600,16 @@ def register_websocket_routes(app) -> None:
|
|
|
572
600
|
|
|
573
601
|
recv_task = asyncio.create_task(_receiver())
|
|
574
602
|
previous_indices = None
|
|
603
|
+
frames_sent = 0
|
|
575
604
|
try:
|
|
576
605
|
while True:
|
|
577
606
|
msg = await _pending.get()
|
|
578
607
|
if msg is None:
|
|
579
608
|
break
|
|
609
|
+
if frames_sent == 0:
|
|
610
|
+
_trace_viewer_event(
|
|
611
|
+
"viewer.first_request", sid, request_id=launch_request_id
|
|
612
|
+
)
|
|
580
613
|
|
|
581
614
|
seq = int(msg.get("seq", 0))
|
|
582
615
|
|
|
@@ -858,6 +891,14 @@ def register_websocket_routes(app) -> None:
|
|
|
858
891
|
raw_payload = payload
|
|
859
892
|
payload = await _render(loop, lambda: _pack_frame(raw_payload))
|
|
860
893
|
await ws.send_bytes(payload)
|
|
894
|
+
frames_sent += 1
|
|
895
|
+
if frames_sent == 1:
|
|
896
|
+
_trace_viewer_event(
|
|
897
|
+
"viewer.first_frame_sent", sid,
|
|
898
|
+
request_id=launch_request_id,
|
|
899
|
+
ms=round((time.perf_counter() - accepted_at) * 1000),
|
|
900
|
+
payload_bytes=len(payload),
|
|
901
|
+
)
|
|
861
902
|
if perf:
|
|
862
903
|
total_ms = (time.perf_counter() - total_t0) * 1000.0
|
|
863
904
|
await ws.send_json(
|
|
@@ -939,6 +980,13 @@ def register_websocket_routes(app) -> None:
|
|
|
939
980
|
|
|
940
981
|
_vprint(f"[ArrayView] WS/{sid[:8]}: {_ws_exc}", flush=True)
|
|
941
982
|
traceback.print_exc()
|
|
983
|
+
_trace_viewer_event(
|
|
984
|
+
"viewer.render_failed", sid,
|
|
985
|
+
request_id=launch_request_id,
|
|
986
|
+
error=type(_ws_exc).__name__,
|
|
987
|
+
message=str(_ws_exc)[:200],
|
|
988
|
+
frames_sent=frames_sent,
|
|
989
|
+
)
|
|
942
990
|
# Without this the viewer never learns the frame it is waiting for
|
|
943
991
|
# will not arrive: the socket just closes and the panel sits on the
|
|
944
992
|
# loading spinner until the opener's timeout fails the request
|
|
@@ -1122,4 +1170,9 @@ def register_websocket_routes(app) -> None:
|
|
|
1122
1170
|
_session_mod.VIEWER_REQUEST_COUNTS[request_key] = request_count
|
|
1123
1171
|
else:
|
|
1124
1172
|
_session_mod.VIEWER_REQUEST_COUNTS.pop(request_key, None)
|
|
1125
|
-
_trace_viewer_event(
|
|
1173
|
+
_trace_viewer_event(
|
|
1174
|
+
"viewer.disconnected", sid,
|
|
1175
|
+
request_id=launch_request_id,
|
|
1176
|
+
frames_sent=frames_sent,
|
|
1177
|
+
open_s=round(time.perf_counter() - accepted_at, 1),
|
|
1178
|
+
)
|
|
@@ -3924,6 +3924,30 @@
|
|
|
3924
3924
|
.compare-title.drag-over { outline: 1px dashed var(--accent, #888); border-radius: 2px; }
|
|
3925
3925
|
.compare-title.compare-center-title { cursor: default; max-width: 100%; overflow: visible; text-overflow: clip; }
|
|
3926
3926
|
.compare-title.compare-side-title { gap: 0; }
|
|
3927
|
+
/* Array names: the start and end every compared name shares fade back
|
|
3928
|
+
and give way first when the slot is narrow (_fitArrayName). */
|
|
3929
|
+
.av-name { min-width: 0; overflow: hidden; white-space: nowrap; }
|
|
3930
|
+
.av-name-shared { opacity: 0.5; }
|
|
3931
|
+
#compare-view-wrap:not(.compare-titles-inset) .compare-title .av-name { color: color-mix(in srgb, var(--text) 75%, var(--muted)); }
|
|
3932
|
+
/* A name beside its image: plain text in the empty margin, wrapping
|
|
3933
|
+
at separators (_placeRowName / _placeCompareTitle). */
|
|
3934
|
+
.av-name.av-name-side {
|
|
3935
|
+
display: block; white-space: normal; overflow-wrap: anywhere;
|
|
3936
|
+
font-size: 12px; line-height: 16px; letter-spacing: 0.02em; text-transform: none;
|
|
3937
|
+
color: var(--text);
|
|
3938
|
+
}
|
|
3939
|
+
#compare-view-wrap.compare-titles-inset .compare-title.compare-title-out-left,
|
|
3940
|
+
#compare-view-wrap.compare-titles-inset .compare-title.compare-title-out-right {
|
|
3941
|
+
top: 50%; transform: translateY(-50%); max-width: none; -webkit-text-stroke: 0; text-shadow: none;
|
|
3942
|
+
}
|
|
3943
|
+
#compare-view-wrap.compare-titles-inset .compare-title.compare-title-out-left { left: auto; right: 100%; margin-right: 14px; justify-content: flex-end; text-align: right; }
|
|
3944
|
+
#compare-view-wrap.compare-titles-inset .compare-title.compare-title-out-right { left: 100%; margin-left: 14px; text-align: left; }
|
|
3945
|
+
.compare-title.compare-title-out-left svg,
|
|
3946
|
+
.compare-title.compare-title-out-right svg { display: none; }
|
|
3947
|
+
.av-name-br { display: none; }
|
|
3948
|
+
/* Chrome breaks at <wbr> even under nowrap: only side names wrap */
|
|
3949
|
+
.av-name:not(.av-name-side) wbr { display: none; }
|
|
3950
|
+
.av-name.av-name-paths .av-name-br { display: block; }
|
|
3927
3951
|
.compare-diff-stats { display: inline-flex; gap: 12px; font-variant-numeric: tabular-nums; cursor: default; }
|
|
3928
3952
|
.compare-diff-stats b { font-weight: normal; color: color-mix(in srgb, var(--text) 75%, var(--muted)); text-transform: none; }
|
|
3929
3953
|
.detached-index-hint {
|
|
@@ -4212,6 +4236,8 @@
|
|
|
4212
4236
|
/* compare-qMRI: row label (array name) at left of each row */
|
|
4213
4237
|
.cq-row-label { font-size: 12px; color: var(--muted); letter-spacing: 0.08em; text-transform: uppercase; writing-mode: vertical-rl; transform: rotate(180deg); width: 24px; flex-shrink: 0; align-self: center; text-align: center; white-space: nowrap; }
|
|
4214
4238
|
.qv-row.cq-row > .cq-row-label { position: absolute; right: 100%; margin-right: 14px; top: 50%; transform: translateY(-50%) rotate(180deg); }
|
|
4239
|
+
/* compare-qMRI / compare-3plane: array name left of its row */
|
|
4240
|
+
.qv-row.cq-row > .cq-row-name { position: absolute; right: 100%; margin-right: 14px; top: 50%; transform: translateY(-50%); text-align: right; cursor: default; user-select: none; }
|
|
4215
4241
|
/* compare-qMRI: column header row for map names */
|
|
4216
4242
|
.cq-header-row { display: flex; flex-direction: row; gap: 24px; justify-content: center; align-items: flex-end; padding-left: 0; }
|
|
4217
4243
|
.cq-header-label { font-size: 16px; color: var(--fg); letter-spacing: 0.05em; text-align: center; }
|
|
@@ -4754,7 +4780,8 @@
|
|
|
4754
4780
|
/* qMRI maps touching (Preferences): no gaps; with two rows the
|
|
4755
4781
|
colour bars float inside the maps like the immersive islands. */
|
|
4756
4782
|
body.qmri-gapless #qmri-view-wrap.active { gap: 0 !important; }
|
|
4757
|
-
body.qmri-gapless #qmri-view-wrap > .qv-row:not(.qmri-mosaic-row)
|
|
4783
|
+
body.qmri-gapless #qmri-view-wrap > .qv-row:not(.qmri-mosaic-row),
|
|
4784
|
+
body.qmri-gapless #qmri-view-wrap > .cq-header-row { gap: 0 !important; }
|
|
4758
4785
|
body.qmri-gapless #qmri-view-wrap .qv-pane { gap: 0; }
|
|
4759
4786
|
body.qmri-gapless #qmri-view-wrap .qv-canvas-clip { border-radius: 0 !important; }
|
|
4760
4787
|
/* Touching panes read as one block: only its outer corners stay
|
|
@@ -9977,6 +10004,231 @@
|
|
|
9977
10004
|
return sidVal.slice(0, 8);
|
|
9978
10005
|
}
|
|
9979
10006
|
|
|
10007
|
+
// Compared names often share most of their text (one file name in
|
|
10008
|
+
// several run folders). Split each into the start and end that every
|
|
10009
|
+
// name shares and the middle that tells it apart. Splits only at
|
|
10010
|
+
// separators, and every name keeps a word of its own.
|
|
10011
|
+
const _NAME_SEP_RE = /^[_\-.\/\\\s()]+$/;
|
|
10012
|
+
function _arrayNameParts(names) {
|
|
10013
|
+
if (!names.length) return [];
|
|
10014
|
+
const toks = names.map(n => String(n).split(/([_\-.\/\\\s()]+)/).filter(Boolean));
|
|
10015
|
+
const hasWord = ts => ts.some(t => !_NAME_SEP_RE.test(t));
|
|
10016
|
+
let h = 0, t = 0;
|
|
10017
|
+
if (toks.length > 1) {
|
|
10018
|
+
const minLen = Math.min(...toks.map(ts => ts.length));
|
|
10019
|
+
while (h < minLen && toks.every(ts => ts[h] === toks[0][h])) h++;
|
|
10020
|
+
while (h > 0 && !toks.every(ts => hasWord(ts.slice(h)))) h--;
|
|
10021
|
+
const rest = toks.map(ts => ts.slice(h));
|
|
10022
|
+
const minRest = Math.min(...rest.map(ts => ts.length));
|
|
10023
|
+
const fromEnd = (ts, i) => ts[ts.length - 1 - i];
|
|
10024
|
+
while (t < minRest && rest.every(ts => fromEnd(ts, t) === fromEnd(rest[0], t))) t++;
|
|
10025
|
+
while (t > 0 && !rest.every(ts => hasWord(ts.slice(0, ts.length - t)))) t--;
|
|
10026
|
+
}
|
|
10027
|
+
// `near`: how much of each key's start it shares with its closest
|
|
10028
|
+
// sibling; in a tight slot that goes before the rest of the key.
|
|
10029
|
+
let keys = toks.map(ts => ts.slice(h, ts.length - t));
|
|
10030
|
+
// Shared folders and a shared file name or extension are left out
|
|
10031
|
+
// (the full path is on hover), but never part of a word group
|
|
10032
|
+
// (pat_a stays pat_a), nor when what remains says nothing on its
|
|
10033
|
+
// own (t1.npy (1) would become "1").
|
|
10034
|
+
const isPath = tok => /[\/\\]/.test(tok);
|
|
10035
|
+
let hd = h;
|
|
10036
|
+
while (hd > 0 && !isPath(toks[0][hd - 1])) hd--;
|
|
10037
|
+
const tailToks = toks[0].slice(toks[0].length - t);
|
|
10038
|
+
const cut = tailToks.findIndex(tok => isPath(tok) || tok.startsWith('.'));
|
|
10039
|
+
const td = cut < 0 ? 0 : t - cut;
|
|
10040
|
+
const shortKeys = toks.map(ts => ts.slice(hd, ts.length - td));
|
|
10041
|
+
const dropShared = shortKeys.every(kt => kt.some(tok => /[a-z]/i.test(tok)));
|
|
10042
|
+
if (dropShared) {
|
|
10043
|
+
keys = shortKeys.map(kt => {
|
|
10044
|
+
let a = 0, b = kt.length;
|
|
10045
|
+
const loose = tok => /^[_\-.\/\\\s]+$/.test(tok);
|
|
10046
|
+
while (a < b && loose(kt[a])) a++;
|
|
10047
|
+
while (b > a && loose(kt[b - 1])) b--;
|
|
10048
|
+
return kt.slice(a, b);
|
|
10049
|
+
});
|
|
10050
|
+
}
|
|
10051
|
+
return toks.map((ts, i) => {
|
|
10052
|
+
let near = 0;
|
|
10053
|
+
keys.forEach((other, j) => {
|
|
10054
|
+
if (j === i) return;
|
|
10055
|
+
let n = 0;
|
|
10056
|
+
while (n < keys[i].length && keys[i][n] === other[n]) n++;
|
|
10057
|
+
while (n > 0 && !hasWord(keys[i].slice(n))) n--;
|
|
10058
|
+
near = Math.max(near, n);
|
|
10059
|
+
});
|
|
10060
|
+
return {
|
|
10061
|
+
head: dropShared ? '' : ts.slice(0, h).join(''),
|
|
10062
|
+
key: keys[i].join(''),
|
|
10063
|
+
tail: dropShared ? '' : ts.slice(ts.length - t).join(''),
|
|
10064
|
+
near: keys[i].slice(0, near).join('').length,
|
|
10065
|
+
};
|
|
10066
|
+
});
|
|
10067
|
+
}
|
|
10068
|
+
|
|
10069
|
+
// Last `n` characters of `s`, moved up to the next word start when the
|
|
10070
|
+
// cut would leave a word fragment (\u2026on_ reads as noise).
|
|
10071
|
+
function _nameTailChars(s, n) {
|
|
10072
|
+
const out = s.slice(s.length - n);
|
|
10073
|
+
if (n >= s.length || /[_\-.\/\\\s()]/.test(s[s.length - n - 1])) return out;
|
|
10074
|
+
const m = out.match(/[_\-.\/\\\s()]+/);
|
|
10075
|
+
return m ? out.slice(m.index + m[0].length) : '';
|
|
10076
|
+
}
|
|
10077
|
+
|
|
10078
|
+
// Name squeezed to `cap` characters: the shared end gives way first,
|
|
10079
|
+
// then the shared start, then the start the name shares with its
|
|
10080
|
+
// closest sibling, and only then the middle of what is its own.
|
|
10081
|
+
// Returns [text, shared] runs.
|
|
10082
|
+
function _arrayNameRuns(p, cap) {
|
|
10083
|
+
const E = '\u2026';
|
|
10084
|
+
const { head, key, tail, near } = p;
|
|
10085
|
+
if (head.length + key.length + tail.length <= cap) return [[head, true], [key, false], [tail, true]];
|
|
10086
|
+
const tailRoom = cap - head.length - key.length - 1;
|
|
10087
|
+
if (tail && tailRoom >= 0) return [[head, true], [key, false], [tail.slice(0, tailRoom) + E, true]];
|
|
10088
|
+
const tailE = tail ? E : '';
|
|
10089
|
+
const headRoom = cap - key.length - tailE.length - 1;
|
|
10090
|
+
if (head && headRoom >= 0) return [[E + _nameTailChars(head, headRoom), true], [key, false], [tailE, true]];
|
|
10091
|
+
if (!head && key.length + tailE.length <= cap) return [[key, false], [tailE, true]];
|
|
10092
|
+
const keyRoom = cap - 1 - tailE.length;
|
|
10093
|
+
if (keyRoom > 0 && key.length - keyRoom <= near) {
|
|
10094
|
+
const cut = _nameTailChars(key, keyRoom);
|
|
10095
|
+
return [[E, true], [cut || key.slice(key.length - keyRoom), false], [tailE, true]];
|
|
10096
|
+
}
|
|
10097
|
+
const rest = key.slice(near).replace(/^[_\-.\/\\\s()]+/, '');
|
|
10098
|
+
const lead = (head || near) ? E : '';
|
|
10099
|
+
const room = cap - lead.length - tailE.length - 1;
|
|
10100
|
+
if (room < 2) return [[rest.slice(0, Math.max(1, cap - 1)), false], [E, true]];
|
|
10101
|
+
const a = Math.floor(room / 2), b = room - a;
|
|
10102
|
+
return [[lead, true], [rest.slice(0, a), false], [E, false], [rest.slice(rest.length - b), false], [tailE, true]];
|
|
10103
|
+
}
|
|
10104
|
+
|
|
10105
|
+
function _arrayNameHTML(runs) {
|
|
10106
|
+
// A name beside its image wraps after separators (not dots: they
|
|
10107
|
+
// sit inside numbers), and after folders when av-name-paths is on.
|
|
10108
|
+
const esc = t => t.replace(/&/g, '&').replace(/</g, '<')
|
|
10109
|
+
.replace(/[_\-\s()]+/g, '$&<wbr>')
|
|
10110
|
+
.replace(/[\/\\]+/g, '$&<span class="av-name-br"></span>');
|
|
10111
|
+
return runs.filter(([t]) => t)
|
|
10112
|
+
.map(([t, shared]) => shared ? `<span class="av-name-shared">${esc(t)}</span>` : esc(t))
|
|
10113
|
+
.join('');
|
|
10114
|
+
}
|
|
10115
|
+
|
|
10116
|
+
// Names sit beside their image, in the empty margin, when it has room
|
|
10117
|
+
// for a column at least NAME_SIDE_MIN_W wide; otherwise inside it.
|
|
10118
|
+
// NAME_SIDE_GAP is also the CSS margin between name and image.
|
|
10119
|
+
const NAME_SIDE_MIN_W = 96;
|
|
10120
|
+
const NAME_SIDE_MAX_W = 280;
|
|
10121
|
+
const NAME_SIDE_GAP = 14;
|
|
10122
|
+
const NAME_SIDE_EDGE = 12;
|
|
10123
|
+
// Margin a row layout keeps free on each side for its row names,
|
|
10124
|
+
// counted from the window edge. Capped so a narrow window keeps its
|
|
10125
|
+
// images and the names get a narrower column instead.
|
|
10126
|
+
function _rowNameReserve() {
|
|
10127
|
+
const view = document.getElementById('qmri-view-wrap');
|
|
10128
|
+
const viewL = view ? Math.max(0, view.getBoundingClientRect().left) : 0;
|
|
10129
|
+
return Math.min(viewL + NAME_SIDE_MIN_W + NAME_SIDE_GAP + NAME_SIDE_EDGE, window.innerWidth * 0.17);
|
|
10130
|
+
}
|
|
10131
|
+
|
|
10132
|
+
function _fitArrayName(el) {
|
|
10133
|
+
const p = el && el._nameParts;
|
|
10134
|
+
if (!p) return;
|
|
10135
|
+
if (el.isConnected && el._namePlace) el._namePlace(el);
|
|
10136
|
+
const full = p.head.length + p.key.length + p.tail.length;
|
|
10137
|
+
const show = cap => { el.innerHTML = _arrayNameHTML(_arrayNameRuns(p, cap)); };
|
|
10138
|
+
el.classList.remove('av-name-paths');
|
|
10139
|
+
show(full);
|
|
10140
|
+
if (!el.isConnected) return;
|
|
10141
|
+
const fits = () => el.scrollWidth <= el.clientWidth && el.scrollHeight <= el.clientHeight;
|
|
10142
|
+
// A name that needs several lines starts each folder on a new one,
|
|
10143
|
+
// unless that alone makes it too tall.
|
|
10144
|
+
if (el.classList.contains('av-name-side') && el.scrollHeight > 1.5 * parseFloat(getComputedStyle(el).lineHeight)) {
|
|
10145
|
+
el.classList.add('av-name-paths');
|
|
10146
|
+
if (!fits()) el.classList.remove('av-name-paths');
|
|
10147
|
+
}
|
|
10148
|
+
if (fits() || el.clientWidth <= 0) return;
|
|
10149
|
+
let lo = 1, hi = full - 1;
|
|
10150
|
+
while (lo < hi) {
|
|
10151
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
10152
|
+
show(mid);
|
|
10153
|
+
if (fits()) lo = mid; else hi = mid - 1;
|
|
10154
|
+
}
|
|
10155
|
+
show(lo);
|
|
10156
|
+
}
|
|
10157
|
+
|
|
10158
|
+
// Refit names when their pane, row or view changes size.
|
|
10159
|
+
const _arrayNameRO = typeof ResizeObserver === 'function'
|
|
10160
|
+
? new ResizeObserver(entries => {
|
|
10161
|
+
const names = new Set();
|
|
10162
|
+
for (const e of entries) {
|
|
10163
|
+
if (!e.target.isConnected) { _arrayNameRO.unobserve(e.target); continue; }
|
|
10164
|
+
e.target.querySelectorAll('.av-name').forEach(n => names.add(n));
|
|
10165
|
+
}
|
|
10166
|
+
names.forEach(_fitArrayName);
|
|
10167
|
+
})
|
|
10168
|
+
: null;
|
|
10169
|
+
|
|
10170
|
+
// Hover text for a shortened name: the file's full path when known.
|
|
10171
|
+
function _arrayNameHover(sidVal, label) {
|
|
10172
|
+
return _compareFilepathBySid[sidVal] || label;
|
|
10173
|
+
}
|
|
10174
|
+
|
|
10175
|
+
function _setArrayName(el, parts, fullName, slots, place) {
|
|
10176
|
+
el.classList.add('av-name');
|
|
10177
|
+
el._nameParts = parts;
|
|
10178
|
+
el._namePlace = place || null;
|
|
10179
|
+
el.title = fullName;
|
|
10180
|
+
_fitArrayName(el);
|
|
10181
|
+
if (_arrayNameRO) slots.forEach(slot => slot && _arrayNameRO.observe(slot));
|
|
10182
|
+
}
|
|
10183
|
+
|
|
10184
|
+
// compare-qMRI / compare-3plane: the name sits left of its row, in
|
|
10185
|
+
// the margin the layout keeps free (_rowNameReserve). The view
|
|
10186
|
+
// clips at its own edge, not the window's.
|
|
10187
|
+
function _placeRowName(el) {
|
|
10188
|
+
const r = el.parentElement.getBoundingClientRect();
|
|
10189
|
+
const viewL = document.getElementById('qmri-view-wrap').getBoundingClientRect().left;
|
|
10190
|
+
const w = Math.min(NAME_SIDE_MAX_W, r.left - viewL - NAME_SIDE_EDGE - NAME_SIDE_GAP);
|
|
10191
|
+
el.style.width = Math.max(40, w) + 'px';
|
|
10192
|
+
el.style.maxHeight = Math.max(16, r.height) + 'px';
|
|
10193
|
+
}
|
|
10194
|
+
|
|
10195
|
+
// Compare panes: names move into the margin beside the panes when
|
|
10196
|
+
// every pane has one (one or two columns) with room; otherwise they
|
|
10197
|
+
// all stay inside their images, never a mix.
|
|
10198
|
+
function _compareTitleSides() {
|
|
10199
|
+
const wrap = document.getElementById('compare-view-wrap');
|
|
10200
|
+
if (!wrap.classList.contains('compare-titles-inset')) return null;
|
|
10201
|
+
const panes = [...wrap.querySelectorAll('.compare-pane.active')]
|
|
10202
|
+
.filter(q => q.getBoundingClientRect().width > 0);
|
|
10203
|
+
if (!panes.length) return null;
|
|
10204
|
+
const rects = panes.map(q => q.getBoundingClientRect());
|
|
10205
|
+
const minL = Math.min(...rects.map(r => r.left));
|
|
10206
|
+
const maxR = Math.max(...rects.map(r => r.right));
|
|
10207
|
+
const roomL = minL - NAME_SIDE_EDGE - NAME_SIDE_GAP;
|
|
10208
|
+
const roomR = window.innerWidth - maxR - NAME_SIDE_EDGE - NAME_SIDE_GAP;
|
|
10209
|
+
const sides = new Map();
|
|
10210
|
+
for (let i = 0; i < panes.length; i++) {
|
|
10211
|
+
const r = rects[i];
|
|
10212
|
+
if (r.left <= minL + 1 && roomL >= NAME_SIDE_MIN_W) sides.set(panes[i], { side: 'left', w: roomL, h: r.height });
|
|
10213
|
+
else if (r.right >= maxR - 1 && roomR >= NAME_SIDE_MIN_W) sides.set(panes[i], { side: 'right', w: roomR, h: r.height });
|
|
10214
|
+
else return null;
|
|
10215
|
+
}
|
|
10216
|
+
return sides;
|
|
10217
|
+
}
|
|
10218
|
+
|
|
10219
|
+
function _placeCompareTitle(el) {
|
|
10220
|
+
const title = el.closest('.compare-title');
|
|
10221
|
+
const pane = title && title.closest('.compare-pane');
|
|
10222
|
+
const sides = pane && !title.classList.contains('compare-side-title') ? _compareTitleSides() : null;
|
|
10223
|
+
const spot = sides && sides.get(pane);
|
|
10224
|
+
const side = spot ? spot.side : null;
|
|
10225
|
+
title.classList.toggle('compare-title-out-left', side === 'left');
|
|
10226
|
+
title.classList.toggle('compare-title-out-right', side === 'right');
|
|
10227
|
+
el.classList.toggle('av-name-side', !!side);
|
|
10228
|
+
el.style.width = side ? Math.min(NAME_SIDE_MAX_W, spot.w) + 'px' : '';
|
|
10229
|
+
el.style.maxHeight = side ? spot.h + 'px' : '';
|
|
10230
|
+
}
|
|
10231
|
+
|
|
9980
10232
|
function _resetDetachedDimState() {
|
|
9981
10233
|
detachedDimMode = false;
|
|
9982
10234
|
detachedDim = -1;
|
|
@@ -10110,14 +10362,17 @@
|
|
|
10110
10362
|
drawRegBlendCb();
|
|
10111
10363
|
}
|
|
10112
10364
|
const _logoSvg = '<svg width="14" height="14" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" style="flex-shrink:0"><rect width="16" height="16" rx="2.5" fill="#0c0c0c"/><rect x="2" y="2" width="3" height="3" fill="#3a0ca3"/><rect x="6.5" y="2" width="3" height="3" fill="#560bad"/><rect x="11" y="2" width="3" height="3" fill="#c77dff"/><rect x="2" y="6.5" width="3" height="3" fill="#4361ee"/><rect x="6.5" y="6.5" width="3" height="3" fill="#4cc9f0"/><rect x="11" y="6.5" width="3" height="3" fill="#f5c842"/><rect x="2" y="11" width="3" height="3" fill="#4895ef"/><rect x="6.5" y="11" width="3" height="3" fill="#80ed99"/><rect x="11" y="11" width="3" height="3" fill="#f8961e"/></svg>';
|
|
10365
|
+
const nameParts = detachedDimMode ? null : _arrayNameParts(labels);
|
|
10113
10366
|
compareTitles.forEach((el, idx) => {
|
|
10114
10367
|
const text = labels[idx] || '';
|
|
10115
10368
|
const sideTitle = _isCompareBigLeftLayout() && idx < 2;
|
|
10116
10369
|
el.classList.toggle('compare-side-title', sideTitle);
|
|
10117
|
-
if (text) {
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
el.innerHTML =
|
|
10370
|
+
if (text && detachedDimMode) {
|
|
10371
|
+
el.innerHTML = '<span>' + _detachedDimPaneTitleHTML(idx, text) + '</span>';
|
|
10372
|
+
} else if (text) {
|
|
10373
|
+
el.innerHTML = (sideTitle ? '' : _logoSvg) + '<span></span>';
|
|
10374
|
+
_setArrayName(el.lastElementChild, nameParts[idx], _arrayNameHover(effectiveSids[idx], text),
|
|
10375
|
+
[el.closest('.compare-pane'), document.getElementById('compare-view-wrap')], _placeCompareTitle);
|
|
10121
10376
|
} else {
|
|
10122
10377
|
el.textContent = '';
|
|
10123
10378
|
}
|
|
@@ -37735,12 +37990,6 @@
|
|
|
37735
37990
|
// Column header row with map names
|
|
37736
37991
|
const headerRow = document.createElement('div');
|
|
37737
37992
|
headerRow.className = 'cq-header-row';
|
|
37738
|
-
// Spacer matching row label width
|
|
37739
|
-
const headerSpacer = document.createElement('div');
|
|
37740
|
-
headerSpacer.className = 'cq-row-label';
|
|
37741
|
-
headerSpacer.style.visibility = 'hidden';
|
|
37742
|
-
headerSpacer.textContent = '\u00A0';
|
|
37743
|
-
headerRow.appendChild(headerSpacer);
|
|
37744
37993
|
for (let mi = 0; mi < nMaps; mi++) {
|
|
37745
37994
|
const hLabel = document.createElement('div');
|
|
37746
37995
|
hLabel.className = 'cq-header-label qv-label';
|
|
@@ -37750,16 +37999,18 @@
|
|
|
37750
37999
|
wrap.appendChild(headerRow);
|
|
37751
38000
|
|
|
37752
38001
|
compareQmriViews = [];
|
|
38002
|
+
const rowNames = compareQmriSids.map(comparePaneLabelForSid);
|
|
38003
|
+
const rowNameParts = _arrayNameParts(rowNames);
|
|
37753
38004
|
// One row per compared array (SID)
|
|
37754
38005
|
for (let si = 0; si < compareQmriSids.length; si++) {
|
|
37755
38006
|
const sidVal = compareQmriSids[si];
|
|
37756
38007
|
const rowDiv = document.createElement('div');
|
|
37757
38008
|
rowDiv.className = 'qv-row cq-row';
|
|
37758
|
-
//
|
|
37759
|
-
const
|
|
37760
|
-
|
|
37761
|
-
|
|
37762
|
-
|
|
38009
|
+
// Array name, left of its row
|
|
38010
|
+
const rowName = document.createElement('div');
|
|
38011
|
+
rowName.className = 'cq-row-name av-name-side';
|
|
38012
|
+
rowDiv.appendChild(rowName);
|
|
38013
|
+
_setArrayName(rowName, rowNameParts[si], _arrayNameHover(sidVal, rowNames[si]), [rowDiv, wrap], _placeRowName);
|
|
37763
38014
|
for (let mi = 0; mi < nMaps; mi++) {
|
|
37764
38015
|
const actualIdx = mapIndices[mi];
|
|
37765
38016
|
const cmap = cmaps[mi];
|
|
@@ -38057,7 +38308,7 @@
|
|
|
38057
38308
|
const rows = nRows;
|
|
38058
38309
|
const colGap = rows >= 3 ? 10 : 24;
|
|
38059
38310
|
const rowGap = rows >= 3 ? 4 : 14;
|
|
38060
|
-
const sidePad = rows >= 3 ? 80 : 120;
|
|
38311
|
+
const sidePad = Math.max(rows >= 3 ? 80 : 120, 2 * _rowNameReserve()); // room for row names
|
|
38061
38312
|
const labelH = 24; // column headers
|
|
38062
38313
|
const anyExpanded = ready.some(v => v._colorBar && v._colorBar._expanded);
|
|
38063
38314
|
const cbH = anyExpanded ? 33 + (CB_DEFAULT_EXPANDED_H - CB_COLLAPSED_H) : 33;
|
|
@@ -38286,23 +38537,18 @@
|
|
|
38286
38537
|
}
|
|
38287
38538
|
|
|
38288
38539
|
compareMvViews = [];
|
|
38540
|
+
const rowNames = compareMvSids.map(comparePaneLabelForSid);
|
|
38541
|
+
const rowNameParts = _arrayNameParts(rowNames);
|
|
38289
38542
|
// One row per compared array (SID)
|
|
38290
38543
|
for (let si = 0; si < compareMvSids.length; si++) {
|
|
38291
38544
|
const sidVal = compareMvSids[si];
|
|
38292
38545
|
const rowDiv = document.createElement('div');
|
|
38293
38546
|
rowDiv.className = 'qv-row cq-row';
|
|
38294
|
-
//
|
|
38295
|
-
const
|
|
38296
|
-
|
|
38297
|
-
|
|
38298
|
-
|
|
38299
|
-
// Basenames alone, unless that would make two rows look alike;
|
|
38300
|
-
// then keep the folders the pane titles use to tell them apart.
|
|
38301
|
-
const _shorts = compareMvSids.map(x => _shortLabel(comparePaneLabelForSid(x)));
|
|
38302
|
-
rowLabel.textContent = _shorts.filter(x => x === _shorts[si]).length > 1
|
|
38303
|
-
? _rawLabel.replace(/\.[^./\\\s]+(\s\(\d+\))?$/, '$1') : _shorts[si];
|
|
38304
|
-
rowLabel.title = _rawLabel; // full name on hover
|
|
38305
|
-
rowDiv.appendChild(rowLabel);
|
|
38547
|
+
// Array name, left of its row
|
|
38548
|
+
const rowName = document.createElement('div');
|
|
38549
|
+
rowName.className = 'cq-row-name av-name-side';
|
|
38550
|
+
rowDiv.appendChild(rowName);
|
|
38551
|
+
_setArrayName(rowName, rowNameParts[si], _arrayNameHover(sidVal, rowNames[si]), [rowDiv, wrap], _placeRowName);
|
|
38306
38552
|
// 3 planes per row
|
|
38307
38553
|
for (let pi = 0; pi < planeDefs.length; pi++) {
|
|
38308
38554
|
const def = planeDefs[pi];
|
|
@@ -38684,7 +38930,7 @@
|
|
|
38684
38930
|
const nCols = 3; // always 3 planes
|
|
38685
38931
|
const colGap = nRows >= 3 ? 12 : 24;
|
|
38686
38932
|
const rowGap = nRows >= 3 ? 6 : 14;
|
|
38687
|
-
const sidePad = nRows >= 3 ? 80 : 120; //
|
|
38933
|
+
const sidePad = Math.max(nRows >= 3 ? 80 : 120, 2 * _rowNameReserve()); // room for row names
|
|
38688
38934
|
const labelH = 0; // no column headers in compare-multiview
|
|
38689
38935
|
const cbWrap = document.getElementById('mv-cb-wrap');
|
|
38690
38936
|
const cbH = cbWrap && colorbarVisible() ? (cbWrap.offsetHeight || 33) : 0;
|
|
@@ -43044,7 +43290,11 @@
|
|
|
43044
43290
|
|
|
43045
43291
|
function _reconcileCompareTitlesInset() {
|
|
43046
43292
|
compareWrap.classList.toggle('compare-touching', _compareTouching());
|
|
43047
|
-
|
|
43293
|
+
const inset = _compareTitlesInset();
|
|
43294
|
+
if (inset !== compareWrap.classList.contains('compare-titles-inset')) {
|
|
43295
|
+
compareWrap.classList.toggle('compare-titles-inset', inset);
|
|
43296
|
+
compareWrap.querySelectorAll('.av-name').forEach(_fitArrayName);
|
|
43297
|
+
}
|
|
43048
43298
|
}
|
|
43049
43299
|
|
|
43050
43300
|
// ── Centralized CB / island visibility reconciler ────────────
|
|
@@ -1234,7 +1234,7 @@ class TestModeCombinations:
|
|
|
1234
1234
|
page.wait_for_function("() => compareActive && compareMvActive", timeout=15_000)
|
|
1235
1235
|
# Panes are titled with the file names, not session ids.
|
|
1236
1236
|
page.wait_for_function(
|
|
1237
|
-
"() => [...document.querySelectorAll('.cq-row-
|
|
1237
|
+
"() => [...document.querySelectorAll('.cq-row-name')].map(e => e.textContent).join(' ').includes('cmportho0')",
|
|
1238
1238
|
timeout=5_000,
|
|
1239
1239
|
)
|
|
1240
1240
|
|
|
@@ -1341,8 +1341,10 @@ class TestModeCombinations:
|
|
|
1341
1341
|
np.save(path, arr_3d)
|
|
1342
1342
|
sids.append(client.post("/load", json={"filepath": str(path), "name": "t1.npy"}).json()["sid"])
|
|
1343
1343
|
page.goto(f"{server_url}/?sid={sids[0]}&compare_sid={sids[1]}&compare_sids={sids[1]}")
|
|
1344
|
+
# Only the part that differs is shown; the full path is on hover.
|
|
1344
1345
|
page.wait_for_function(
|
|
1345
|
-
"""() => { const
|
|
1346
|
-
return
|
|
1346
|
+
"""() => { const n = document.querySelector('#compare-left-title .av-name');
|
|
1347
|
+
return n && n.textContent === 'pat_a'
|
|
1348
|
+
&& n.title.endsWith('pat_a/t1.npy'); }""",
|
|
1347
1349
|
timeout=15_000,
|
|
1348
1350
|
)
|
|
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
|
|
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
|