arrayview 0.12.2__tar.gz → 0.13.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.12.2 → arrayview-0.13.0}/PKG-INFO +1 -1
- {arrayview-0.12.2 → arrayview-0.13.0}/pyproject.toml +1 -1
- {arrayview-0.12.2 → arrayview-0.13.0}/scripts/release.sh +1 -1
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_launcher.py +79 -18
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_stdio_server.py +278 -1
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_vscode.py +60 -9
- arrayview-0.13.0/src/arrayview/arrayview-opener.vsix +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/uv.lock +1 -1
- {arrayview-0.12.2 → arrayview-0.13.0}/vscode-extension/extension.js +151 -39
- arrayview-0.13.0/vscode-extension/package.json +53 -0
- arrayview-0.12.2/src/arrayview/arrayview-opener.vsix +0 -0
- arrayview-0.12.2/vscode-extension/package.json +0 -30
- {arrayview-0.12.2 → arrayview-0.13.0}/.claude/skills/invocation-consistency/SKILL.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/.claude/skills/modes-consistency/SKILL.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/.claude/skills/ui-consistency-audit/SKILL.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/.claude/skills/viewer-ui-checklist/SKILL.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/.claude/skills/visual-bug-fixing/SKILL.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/.github/workflows/docs.yml +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/.github/workflows/python-publish.yml +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/.gitignore +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/.python-version +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/AGENTS.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/CONTRIBUTING.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/LICENSE +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/README.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/docs/comparing.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/docs/configuration.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/docs/display.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/docs/index.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/docs/loading.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/docs/logo.png +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/docs/measurement.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/docs/remote.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/docs/stylesheets/extra.css +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/docs/viewing.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/matlab/arrayview.m +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/mkdocs.yml +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/plans/webview/LOG.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/scripts/demo.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/ARCHITECTURE.md +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/__init__.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/__main__.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_app.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_config.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_icon.png +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_io.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_platform.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_render.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_segmentation.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_server.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_session.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_shell.html +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_torch.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_viewer.html +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/tests/conftest.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/tests/make_vectorfield_test_arrays.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_api.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_browser.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_cli.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_command_reachability.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_config.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_interactions.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_large_arrays.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_mode_consistency.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_mode_matrix.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_mode_roundtrip.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_nifti_meta.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_rgb_pixel_art.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_torch.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/tests/ui_audit.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/tests/visual_smoke.py +0 -0
- {arrayview-0.12.2 → arrayview-0.13.0}/vscode-extension/LICENSE +0 -0
|
@@ -66,7 +66,7 @@ run() {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
# --- Generate release notes ---
|
|
69
|
-
CLAUDE_BIN="${CLAUDE_BIN
|
|
69
|
+
CLAUDE_BIN="${CLAUDE_BIN:-$(command -v claude 2>/dev/null || echo claude)}"
|
|
70
70
|
PREV_TAG=$(git describe --tags --abbrev=0 HEAD 2>/dev/null || echo "")
|
|
71
71
|
NOTES=""
|
|
72
72
|
|
|
@@ -2102,24 +2102,67 @@ def arrayview():
|
|
|
2102
2102
|
from arrayview._io import load_data
|
|
2103
2103
|
from arrayview._session import SESSIONS, Session
|
|
2104
2104
|
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2105
|
+
# First file is the main array
|
|
2106
|
+
base_path = args.files[0]
|
|
2107
|
+
data = load_data(base_path)
|
|
2108
|
+
session = Session(
|
|
2109
|
+
data=data,
|
|
2110
|
+
filepath=base_path,
|
|
2111
|
+
name=_Path(base_path).name,
|
|
2112
|
+
)
|
|
2113
|
+
if getattr(args, "rgb", False):
|
|
2114
|
+
_setup_rgb(session)
|
|
2115
|
+
|
|
2116
|
+
# Attach vector field if provided
|
|
2117
|
+
if getattr(args, "vectorfield", None):
|
|
2118
|
+
from arrayview._server import _configure_vectorfield
|
|
2119
|
+
|
|
2120
|
+
vf_data = load_data(args.vectorfield)
|
|
2121
|
+
_configure_vectorfield(
|
|
2122
|
+
session, vf_data,
|
|
2123
|
+
getattr(args, "vectorfield_components_dim", None),
|
|
2111
2124
|
)
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2125
|
+
|
|
2126
|
+
SESSIONS[session.sid] = session
|
|
2127
|
+
|
|
2128
|
+
# Load overlay as a separate session
|
|
2129
|
+
overlay_sid = None
|
|
2130
|
+
if getattr(args, "overlay", None):
|
|
2131
|
+
ov_data = load_data(args.overlay)
|
|
2132
|
+
ov_session = Session(
|
|
2133
|
+
data=ov_data,
|
|
2134
|
+
filepath=args.overlay,
|
|
2135
|
+
name="overlay",
|
|
2121
2136
|
)
|
|
2122
|
-
|
|
2137
|
+
SESSIONS[ov_session.sid] = ov_session
|
|
2138
|
+
overlay_sid = ov_session.sid
|
|
2139
|
+
|
|
2140
|
+
# Load compare files (additional positional args + --compare)
|
|
2141
|
+
compare_sids = []
|
|
2142
|
+
compare_paths = list(args.files[1:])
|
|
2143
|
+
if getattr(args, "compare", None):
|
|
2144
|
+
compare_paths.append(args.compare)
|
|
2145
|
+
for cp in compare_paths:
|
|
2146
|
+
cmp_data = load_data(cp)
|
|
2147
|
+
cmp_session = Session(
|
|
2148
|
+
data=cmp_data,
|
|
2149
|
+
filepath=cp,
|
|
2150
|
+
name=_Path(cp).name,
|
|
2151
|
+
)
|
|
2152
|
+
SESSIONS[cmp_session.sid] = cmp_session
|
|
2153
|
+
compare_sids.append(cmp_session.sid)
|
|
2154
|
+
|
|
2155
|
+
info = json.dumps(
|
|
2156
|
+
{
|
|
2157
|
+
"sid": session.sid,
|
|
2158
|
+
"name": session.name,
|
|
2159
|
+
"shape": [int(s) for s in session.shape],
|
|
2160
|
+
"has_vectorfield": session.vfield is not None,
|
|
2161
|
+
"overlay_sid": overlay_sid,
|
|
2162
|
+
"compare_sids": compare_sids or None,
|
|
2163
|
+
}
|
|
2164
|
+
)
|
|
2165
|
+
print(f"SESSION:{info}", file=sys.stderr)
|
|
2123
2166
|
run_stdio_server()
|
|
2124
2167
|
return
|
|
2125
2168
|
|
|
@@ -2662,9 +2705,27 @@ def arrayview():
|
|
|
2662
2705
|
# WebSocket server entirely. The extension spawns a Python subprocess
|
|
2663
2706
|
# with --mode stdio and bridges via postMessage — no ports needed.
|
|
2664
2707
|
is_remote = _is_vscode_remote()
|
|
2665
|
-
if is_remote
|
|
2708
|
+
if is_remote:
|
|
2666
2709
|
_ensure_vscode_extension()
|
|
2667
|
-
|
|
2710
|
+
# Build generic extra CLI args so new flags work without touching the
|
|
2711
|
+
# VS Code extension — they're forwarded verbatim to the subprocess.
|
|
2712
|
+
extra_args: list[str] = []
|
|
2713
|
+
if args.vectorfield:
|
|
2714
|
+
extra_args += ["--vectorfield", os.path.abspath(args.vectorfield)]
|
|
2715
|
+
if vfield_components_dim is not None:
|
|
2716
|
+
extra_args += ["--vectorfield-components-dim", str(vfield_components_dim)]
|
|
2717
|
+
if args.overlay:
|
|
2718
|
+
extra_args += ["--overlay", os.path.abspath(args.overlay)]
|
|
2719
|
+
if args.rgb:
|
|
2720
|
+
extra_args.append("--rgb")
|
|
2721
|
+
# Compare files: pass as additional positional args
|
|
2722
|
+
for cf in compare_files:
|
|
2723
|
+
extra_args.append(cf)
|
|
2724
|
+
_open_direct_via_signal_file(
|
|
2725
|
+
base_file,
|
|
2726
|
+
title=f"ArrayView: {name}",
|
|
2727
|
+
extra_args=extra_args or None,
|
|
2728
|
+
)
|
|
2668
2729
|
return
|
|
2669
2730
|
|
|
2670
2731
|
sid = uuid.uuid4().hex
|
|
@@ -24,7 +24,10 @@ import numpy as np
|
|
|
24
24
|
|
|
25
25
|
from arrayview._io import load_data, load_data_with_meta
|
|
26
26
|
from arrayview._render import (
|
|
27
|
+
LUTS,
|
|
27
28
|
_composite_overlay_mask,
|
|
29
|
+
_compute_vmin_vmax,
|
|
30
|
+
_ensure_lut,
|
|
28
31
|
_extract_overlay_mask,
|
|
29
32
|
_init_luts,
|
|
30
33
|
_overlay_is_label_map,
|
|
@@ -32,6 +35,7 @@ from arrayview._render import (
|
|
|
32
35
|
apply_complex_mode,
|
|
33
36
|
extract_projection,
|
|
34
37
|
extract_slice,
|
|
38
|
+
mosaic_shape,
|
|
35
39
|
render_mosaic,
|
|
36
40
|
render_projection_rgba,
|
|
37
41
|
render_rgb_rgba,
|
|
@@ -485,6 +489,258 @@ def _handle_lebesgue(sid: str, params: dict[str, str]) -> None:
|
|
|
485
489
|
})
|
|
486
490
|
|
|
487
491
|
|
|
492
|
+
def _handle_vectorfield_fetch(sid: str, params: dict) -> None:
|
|
493
|
+
"""Return downsampled vector field arrows for a 2-D view."""
|
|
494
|
+
session = SESSIONS.get(sid)
|
|
495
|
+
if not session or session.vfield is None:
|
|
496
|
+
_write_json({"error": "no vectorfield"})
|
|
497
|
+
return
|
|
498
|
+
dim_x = int(params.get("dim_x", 0))
|
|
499
|
+
dim_y = int(params.get("dim_y", 0))
|
|
500
|
+
indices = params.get("indices", "")
|
|
501
|
+
idx_tuple = tuple(int(x) for x in indices.split(",")) if indices else ()
|
|
502
|
+
t_index = int(params.get("t_index", 0))
|
|
503
|
+
density_offset = int(params.get("density_offset", 0))
|
|
504
|
+
result = _compute_vfield_arrows(session, dim_x, dim_y, idx_tuple, t_index, density_offset)
|
|
505
|
+
if result is None:
|
|
506
|
+
_write_json({"error": "vectorfield computation failed"})
|
|
507
|
+
return
|
|
508
|
+
_write_json({
|
|
509
|
+
"arrows": result["arrows"].tolist(),
|
|
510
|
+
"scale": result["scale"],
|
|
511
|
+
"stride": result["stride"],
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _handle_pixel(sid: str, params: dict) -> None:
|
|
516
|
+
"""Return the raw value at a single pixel."""
|
|
517
|
+
session = SESSIONS.get(sid)
|
|
518
|
+
if not session:
|
|
519
|
+
_write_json({"error": "session not found"})
|
|
520
|
+
return
|
|
521
|
+
if session.rgb_axis is not None:
|
|
522
|
+
_write_json({"value": None})
|
|
523
|
+
return
|
|
524
|
+
dim_x = int(params.get("dim_x", 0))
|
|
525
|
+
dim_y = int(params.get("dim_y", 0))
|
|
526
|
+
indices = params.get("indices", "")
|
|
527
|
+
idx_tuple = tuple(int(x) for x in indices.split(",")) if indices else ()
|
|
528
|
+
px = int(params.get("px", 0))
|
|
529
|
+
py = int(params.get("py", 0))
|
|
530
|
+
complex_mode = int(params.get("complex_mode", 0))
|
|
531
|
+
raw = extract_slice(session, dim_x, dim_y, list(idx_tuple))
|
|
532
|
+
data = apply_complex_mode(raw, complex_mode)
|
|
533
|
+
h, w = data.shape
|
|
534
|
+
if 0 <= py < h and 0 <= px < w:
|
|
535
|
+
v = data[py, px]
|
|
536
|
+
val = None if np.isnan(v) or np.isinf(v) else float(v)
|
|
537
|
+
else:
|
|
538
|
+
val = None
|
|
539
|
+
_write_json({"value": val})
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
# ---------------------------------------------------------------------------
|
|
543
|
+
# Diff / Compare helpers
|
|
544
|
+
# ---------------------------------------------------------------------------
|
|
545
|
+
|
|
546
|
+
def _render_normalized(session, dim_x, dim_y, idx_tuple, dr, complex_mode, log_scale):
|
|
547
|
+
"""Extract a slice and normalize to [0, 1] float32."""
|
|
548
|
+
raw = extract_slice(session, dim_x, dim_y, list(idx_tuple))
|
|
549
|
+
data, vmin, vmax = _prepare_display(session, raw, complex_mode, dr, log_scale)
|
|
550
|
+
if vmax > vmin:
|
|
551
|
+
normalized = np.clip((data - vmin) / (vmax - vmin), 0, 1)
|
|
552
|
+
else:
|
|
553
|
+
normalized = np.zeros_like(data)
|
|
554
|
+
return normalized.astype(np.float32)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _render_normalized_mosaic(session, dim_x, dim_y, dim_z, idx_tuple, dr, complex_mode, log_scale):
|
|
558
|
+
"""Return (float32 normalized mosaic [0,1], nan_mask)."""
|
|
559
|
+
n = session.shape[dim_z]
|
|
560
|
+
idx_list = list(idx_tuple)
|
|
561
|
+
frames_raw = [
|
|
562
|
+
extract_slice(
|
|
563
|
+
session, dim_x, dim_y,
|
|
564
|
+
[i if j == dim_z else idx_list[j] for j in range(len(session.shape))],
|
|
565
|
+
)
|
|
566
|
+
for i in range(n)
|
|
567
|
+
]
|
|
568
|
+
frames = [apply_complex_mode(f, complex_mode) for f in frames_raw]
|
|
569
|
+
if log_scale:
|
|
570
|
+
frames = [np.log1p(np.abs(f)).astype(np.float32) for f in frames]
|
|
571
|
+
all_data = np.stack(frames)
|
|
572
|
+
if log_scale:
|
|
573
|
+
vmin = float(np.percentile(all_data, 1))
|
|
574
|
+
vmax = float(np.percentile(all_data, 99))
|
|
575
|
+
else:
|
|
576
|
+
vmin, vmax = _compute_vmin_vmax(session, all_data, dr, complex_mode)
|
|
577
|
+
rows, cols = mosaic_shape(n)
|
|
578
|
+
H, W = frames[0].shape
|
|
579
|
+
GAP = 2
|
|
580
|
+
total_h = rows * H + (rows - 1) * GAP
|
|
581
|
+
total_w = cols * W + (cols - 1) * GAP
|
|
582
|
+
grid = np.full((total_h, total_w), np.nan, dtype=np.float32)
|
|
583
|
+
for k in range(n):
|
|
584
|
+
r, c = divmod(k, cols)
|
|
585
|
+
r0, c0 = r * (H + GAP), c * (W + GAP)
|
|
586
|
+
grid[r0 : r0 + H, c0 : c0 + W] = all_data[k]
|
|
587
|
+
nan_mask = np.isnan(grid)
|
|
588
|
+
if vmax > vmin:
|
|
589
|
+
normalized = np.clip(np.where(nan_mask, 0.0, (grid - vmin) / (vmax - vmin)), 0, 1)
|
|
590
|
+
else:
|
|
591
|
+
normalized = np.zeros_like(grid)
|
|
592
|
+
return normalized.astype(np.float32), nan_mask
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _compute_diff(session_a, session_b, dim_x, dim_y, indices, dim_z, dr, complex_mode, log_scale, diff_mode):
|
|
596
|
+
"""Shared diff logic for both diff image and diff histogram handlers.
|
|
597
|
+
|
|
598
|
+
Returns (raw_diff, vmin, vmax, colormap, nan_mask_or_None).
|
|
599
|
+
"""
|
|
600
|
+
idx_tuple = tuple(int(x) for x in indices.split(",")) if isinstance(indices, str) else indices
|
|
601
|
+
ndim_a = len(session_a.shape)
|
|
602
|
+
ndim_b = len(session_b.shape)
|
|
603
|
+
idx_a = idx_tuple[:ndim_a]
|
|
604
|
+
idx_b = idx_tuple[:ndim_b]
|
|
605
|
+
nan_mask = None
|
|
606
|
+
|
|
607
|
+
if dim_z >= 0:
|
|
608
|
+
a, nan_mask_a = _render_normalized_mosaic(session_a, dim_x, dim_y, dim_z, idx_a, dr, complex_mode, log_scale)
|
|
609
|
+
b, nan_mask_b = _render_normalized_mosaic(session_b, dim_x, dim_y, dim_z, idx_b, dr, complex_mode, log_scale)
|
|
610
|
+
nan_mask = nan_mask_a | nan_mask_b
|
|
611
|
+
else:
|
|
612
|
+
a = _render_normalized(session_a, dim_x, dim_y, idx_a, dr, complex_mode, log_scale)
|
|
613
|
+
b = _render_normalized(session_b, dim_x, dim_y, idx_b, dr, complex_mode, log_scale)
|
|
614
|
+
|
|
615
|
+
# Resize b to match a if shapes differ
|
|
616
|
+
if a.shape != b.shape:
|
|
617
|
+
b_img = _pil_image().fromarray((b * 255).astype(np.uint8), mode="L")
|
|
618
|
+
b_img = b_img.resize((a.shape[1], a.shape[0]), _pil_image().BILINEAR)
|
|
619
|
+
b = np.array(b_img, dtype=np.float32) / 255.0
|
|
620
|
+
|
|
621
|
+
if diff_mode == 1:
|
|
622
|
+
raw = a - b
|
|
623
|
+
vmin, vmax = -1.0, 1.0
|
|
624
|
+
colormap = "RdBu_r"
|
|
625
|
+
elif diff_mode == 2:
|
|
626
|
+
raw = np.abs(a - b)
|
|
627
|
+
vmax = float(raw.max()) or 1.0
|
|
628
|
+
vmin = 0.0
|
|
629
|
+
colormap = "afmhot"
|
|
630
|
+
else:
|
|
631
|
+
raw = np.abs(a - b) / np.maximum(np.abs(a), 1e-6)
|
|
632
|
+
raw = np.clip(raw, 0.0, 2.0).astype(np.float32)
|
|
633
|
+
vmax = float(raw.max()) or 1.0
|
|
634
|
+
vmin = 0.0
|
|
635
|
+
colormap = "afmhot"
|
|
636
|
+
|
|
637
|
+
return raw, vmin, vmax, colormap, nan_mask
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _handle_diff(sid_a: str, sid_b: str, params: dict) -> None:
|
|
641
|
+
"""Render a diff image and return as base64 JPEG with metadata headers."""
|
|
642
|
+
import base64
|
|
643
|
+
import io
|
|
644
|
+
|
|
645
|
+
session_a = SESSIONS.get(sid_a)
|
|
646
|
+
session_b = SESSIONS.get(sid_b)
|
|
647
|
+
if not session_a or not session_b:
|
|
648
|
+
_write_json({"error": "session not found"})
|
|
649
|
+
return
|
|
650
|
+
|
|
651
|
+
dim_x = int(params.get("dim_x", 0))
|
|
652
|
+
dim_y = int(params.get("dim_y", 0))
|
|
653
|
+
indices = params.get("indices", "")
|
|
654
|
+
dim_z = int(params.get("dim_z", -1))
|
|
655
|
+
dr = int(params.get("dr", 1))
|
|
656
|
+
complex_mode = int(params.get("complex_mode", 0))
|
|
657
|
+
log_scale = params.get("log_scale", "false").lower() == "true"
|
|
658
|
+
diff_mode = int(params.get("diff_mode", 1))
|
|
659
|
+
diff_colormap = params.get("diff_colormap", "")
|
|
660
|
+
_vmin_ov = params.get("vmin_override")
|
|
661
|
+
_vmax_ov = params.get("vmax_override")
|
|
662
|
+
vmin_override = float(_vmin_ov) if _vmin_ov is not None else None
|
|
663
|
+
vmax_override = float(_vmax_ov) if _vmax_ov is not None else None
|
|
664
|
+
|
|
665
|
+
try:
|
|
666
|
+
raw, vmin, vmax, colormap, nan_mask = _compute_diff(
|
|
667
|
+
session_a, session_b, dim_x, dim_y, indices,
|
|
668
|
+
dim_z, dr, complex_mode, log_scale, diff_mode,
|
|
669
|
+
)
|
|
670
|
+
except Exception as e:
|
|
671
|
+
_write_json({"error": str(e)})
|
|
672
|
+
return
|
|
673
|
+
|
|
674
|
+
if vmin_override is not None:
|
|
675
|
+
vmin = vmin_override
|
|
676
|
+
if vmax_override is not None:
|
|
677
|
+
vmax = vmax_override
|
|
678
|
+
if diff_colormap and _ensure_lut(diff_colormap):
|
|
679
|
+
colormap = diff_colormap
|
|
680
|
+
|
|
681
|
+
if vmax > vmin:
|
|
682
|
+
normalized = np.clip((raw - vmin) / (vmax - vmin), 0, 1)
|
|
683
|
+
else:
|
|
684
|
+
normalized = np.zeros_like(raw)
|
|
685
|
+
_ensure_lut(colormap)
|
|
686
|
+
lut = LUTS.get(colormap, LUTS["gray"])
|
|
687
|
+
rgba = lut[(normalized * 255).astype(np.uint8)]
|
|
688
|
+
if nan_mask is not None and nan_mask.shape == rgba.shape[:2]:
|
|
689
|
+
rgba[nan_mask] = [22, 22, 22, 255]
|
|
690
|
+
|
|
691
|
+
img = _pil_image().fromarray(rgba[:, :, :3], mode="RGB")
|
|
692
|
+
buf = io.BytesIO()
|
|
693
|
+
img.save(buf, format="JPEG", quality=90)
|
|
694
|
+
|
|
695
|
+
_write_json({
|
|
696
|
+
"_binary": True,
|
|
697
|
+
"data": base64.b64encode(buf.getvalue()).decode("ascii"),
|
|
698
|
+
"headers": {
|
|
699
|
+
"X-ArrayView-Vmin": str(vmin),
|
|
700
|
+
"X-ArrayView-Vmax": str(vmax),
|
|
701
|
+
"X-ArrayView-Colormap": colormap,
|
|
702
|
+
},
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _handle_diff_histogram(sid_a: str, sid_b: str, params: dict) -> None:
|
|
707
|
+
"""Compute histogram of the diff between two sessions."""
|
|
708
|
+
session_a = SESSIONS.get(sid_a)
|
|
709
|
+
session_b = SESSIONS.get(sid_b)
|
|
710
|
+
if not session_a or not session_b:
|
|
711
|
+
_write_json({"error": "session not found"})
|
|
712
|
+
return
|
|
713
|
+
|
|
714
|
+
dim_x = int(params.get("dim_x", 0))
|
|
715
|
+
dim_y = int(params.get("dim_y", 0))
|
|
716
|
+
indices = params.get("indices", "")
|
|
717
|
+
dim_z = int(params.get("dim_z", -1))
|
|
718
|
+
dr = int(params.get("dr", 1))
|
|
719
|
+
complex_mode = int(params.get("complex_mode", 0))
|
|
720
|
+
log_scale = params.get("log_scale", "false").lower() == "true"
|
|
721
|
+
diff_mode = int(params.get("diff_mode", 1))
|
|
722
|
+
bins = int(params.get("bins", 64))
|
|
723
|
+
|
|
724
|
+
try:
|
|
725
|
+
raw, _, _, _, _ = _compute_diff(
|
|
726
|
+
session_a, session_b, dim_x, dim_y, indices,
|
|
727
|
+
dim_z, dr, complex_mode, log_scale, diff_mode,
|
|
728
|
+
)
|
|
729
|
+
except Exception as e:
|
|
730
|
+
_write_json({"error": str(e)})
|
|
731
|
+
return
|
|
732
|
+
|
|
733
|
+
vmin = float(raw.min())
|
|
734
|
+
vmax = float(raw.max())
|
|
735
|
+
counts, edges = np.histogram(raw.ravel(), bins=bins)
|
|
736
|
+
_write_json({
|
|
737
|
+
"counts": counts.tolist(),
|
|
738
|
+
"edges": edges.tolist(),
|
|
739
|
+
"vmin": vmin,
|
|
740
|
+
"vmax": vmax,
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
|
|
488
744
|
def _handle_fetch_proxy(msg: dict) -> None:
|
|
489
745
|
"""Handle proxied fetch requests from the viewer."""
|
|
490
746
|
endpoint = msg.get("endpoint", "")
|
|
@@ -516,6 +772,14 @@ def _handle_fetch_proxy(msg: dict) -> None:
|
|
|
516
772
|
_write_json({"status": "started"})
|
|
517
773
|
elif route == "preload_status" and sid:
|
|
518
774
|
_write_json({"done": 0, "total": 0, "skipped": True})
|
|
775
|
+
elif route == "vectorfield" and sid:
|
|
776
|
+
_handle_vectorfield_fetch(sid, params)
|
|
777
|
+
elif route == "pixel" and sid:
|
|
778
|
+
_handle_pixel(sid, params)
|
|
779
|
+
elif route == "diff" and len(parts) >= 3:
|
|
780
|
+
_handle_diff(parts[1], parts[2], params)
|
|
781
|
+
elif route == "diff-histogram" and len(parts) >= 3:
|
|
782
|
+
_handle_diff_histogram(parts[1], parts[2], params)
|
|
519
783
|
else:
|
|
520
784
|
_write_error(f"unsupported endpoint: {endpoint}")
|
|
521
785
|
|
|
@@ -648,7 +912,20 @@ def _handle_get_viewer_html(msg: dict) -> None:
|
|
|
648
912
|
|
|
649
913
|
sid = msg.get("sid", "")
|
|
650
914
|
|
|
651
|
-
|
|
915
|
+
# Build query string from session info — the extension forwards the full
|
|
916
|
+
# SESSION: payload so new features (overlay_sid, compare, etc.) work
|
|
917
|
+
# without touching extension code.
|
|
918
|
+
qs = f"?sid={sid}&transport=postMessage"
|
|
919
|
+
overlay_sid = msg.get("overlay_sid")
|
|
920
|
+
if overlay_sid:
|
|
921
|
+
qs += f"&overlay_sid={overlay_sid}"
|
|
922
|
+
compare_sids = msg.get("compare_sids")
|
|
923
|
+
if compare_sids:
|
|
924
|
+
sids = compare_sids if isinstance(compare_sids, list) else [s.strip() for s in str(compare_sids).split(",") if s.strip()]
|
|
925
|
+
if sids:
|
|
926
|
+
qs += f"&compare_sid={sids[0]}"
|
|
927
|
+
qs += f"&compare_sids={','.join(sids)}"
|
|
928
|
+
query_val = json.dumps(qs) if sid else "null"
|
|
652
929
|
|
|
653
930
|
_theme_names = ["dark", "light", "solarized", "nord"]
|
|
654
931
|
_cfg_theme = get_viewer_theme()
|
|
@@ -58,7 +58,7 @@ def _vscode_app_bundle() -> str | None:
|
|
|
58
58
|
|
|
59
59
|
_VSCODE_EXT_INSTALLED = False # cached so we only check once per process
|
|
60
60
|
_VSCODE_EXT_FRESH_INSTALL = False # True if we just installed it this session
|
|
61
|
-
_VSCODE_EXT_VERSION = "0.
|
|
61
|
+
_VSCODE_EXT_VERSION = "0.13.0" # current bundled extension version
|
|
62
62
|
_VSCODE_SIGNAL_FILENAME = "open-request-v0900.json"
|
|
63
63
|
_VSCODE_COMPAT_SIGNAL_FILENAMES: tuple[str, ...] = ("open-request-v0800.json",)
|
|
64
64
|
_VSCODE_PORT_SETTINGS_SETTLE_SECONDS = 2.0
|
|
@@ -152,14 +152,42 @@ def _remove_old_extension_versions(current_version: str) -> None:
|
|
|
152
152
|
_vprint(f"[ArrayView] could not remove {entry}: {exc}", flush=True)
|
|
153
153
|
|
|
154
154
|
|
|
155
|
-
def _extension_on_disk(version: str) -> bool:
|
|
156
|
-
"""Return True if the extension directory for *version* exists on disk.
|
|
155
|
+
def _extension_on_disk(version: str, vsix_path: str | None = None) -> bool:
|
|
156
|
+
"""Return True if the extension directory for *version* exists on disk.
|
|
157
|
+
|
|
158
|
+
When *vsix_path* is given, also verifies that the installed extension
|
|
159
|
+
matches the bundled VSIX by comparing a content hash stored at install
|
|
160
|
+
time. This catches rebuilds during development where the version stays
|
|
161
|
+
the same but the VSIX content changed.
|
|
162
|
+
"""
|
|
163
|
+
import hashlib
|
|
164
|
+
|
|
157
165
|
for base in (
|
|
158
166
|
os.path.expanduser("~/.vscode/extensions"),
|
|
159
167
|
os.path.expanduser("~/.vscode-server/extensions"),
|
|
160
168
|
):
|
|
161
|
-
|
|
169
|
+
ext_dir = os.path.join(base, f"arrayview.arrayview-opener-{version}")
|
|
170
|
+
if not os.path.isdir(ext_dir):
|
|
171
|
+
continue
|
|
172
|
+
if vsix_path is None:
|
|
173
|
+
return True
|
|
174
|
+
# Compare content hash
|
|
175
|
+
try:
|
|
176
|
+
vsix_hash = hashlib.md5(open(vsix_path, "rb").read()).hexdigest()
|
|
177
|
+
except OSError:
|
|
178
|
+
return True # can't read VSIX, assume installed is fine
|
|
179
|
+
hash_file = os.path.join(ext_dir, ".vsix_hash")
|
|
180
|
+
try:
|
|
181
|
+
installed_hash = open(hash_file).read().strip()
|
|
182
|
+
except OSError:
|
|
183
|
+
installed_hash = None
|
|
184
|
+
if installed_hash == vsix_hash:
|
|
162
185
|
return True
|
|
186
|
+
_vprint(
|
|
187
|
+
f"[ArrayView] VSIX content changed (installed={installed_hash}, bundled={vsix_hash}) — reinstalling",
|
|
188
|
+
flush=True,
|
|
189
|
+
)
|
|
190
|
+
return False
|
|
163
191
|
return False
|
|
164
192
|
|
|
165
193
|
|
|
@@ -190,11 +218,11 @@ def _ensure_vscode_extension() -> bool:
|
|
|
190
218
|
_vprint("[ArrayView] could not read version from bundled VSIX", flush=True)
|
|
191
219
|
return False
|
|
192
220
|
|
|
193
|
-
# Fast path: correct version already installed — no reinstall
|
|
194
|
-
# Reinstalling with --force triggers an extension-host reload,
|
|
195
|
-
# creates a ~10-15s gap during which the signal file can be missed.
|
|
221
|
+
# Fast path: correct version and content already installed — no reinstall
|
|
222
|
+
# needed. Reinstalling with --force triggers an extension-host reload,
|
|
223
|
+
# which creates a ~10-15s gap during which the signal file can be missed.
|
|
196
224
|
_remove_old_extension_versions(ext_version)
|
|
197
|
-
if _extension_on_disk(ext_version):
|
|
225
|
+
if _extension_on_disk(ext_version, vsix_path):
|
|
198
226
|
_VSCODE_EXT_INSTALLED = True
|
|
199
227
|
_vprint(
|
|
200
228
|
f"[ArrayView] extension v{ext_version} already installed — skipping reinstall",
|
|
@@ -228,6 +256,21 @@ def _ensure_vscode_extension() -> bool:
|
|
|
228
256
|
)
|
|
229
257
|
if r.returncode == 0 and not install_failed:
|
|
230
258
|
_patch_vscode_extension_metadata(ext_version)
|
|
259
|
+
# Write content hash so future runs can detect VSIX rebuilds
|
|
260
|
+
# without a version bump (common during development).
|
|
261
|
+
try:
|
|
262
|
+
import hashlib
|
|
263
|
+
vsix_hash = hashlib.md5(open(vsix_path, "rb").read()).hexdigest()
|
|
264
|
+
for base in (
|
|
265
|
+
os.path.expanduser("~/.vscode/extensions"),
|
|
266
|
+
os.path.expanduser("~/.vscode-server/extensions"),
|
|
267
|
+
):
|
|
268
|
+
ext_dir = os.path.join(base, f"arrayview.arrayview-opener-{ext_version}")
|
|
269
|
+
if os.path.isdir(ext_dir):
|
|
270
|
+
with open(os.path.join(ext_dir, ".vsix_hash"), "w") as f:
|
|
271
|
+
f.write(vsix_hash)
|
|
272
|
+
except Exception:
|
|
273
|
+
pass # non-critical
|
|
231
274
|
_VSCODE_EXT_INSTALLED = True
|
|
232
275
|
_VSCODE_EXT_FRESH_INSTALL = True
|
|
233
276
|
return True
|
|
@@ -566,7 +609,9 @@ def _open_via_signal_file(
|
|
|
566
609
|
|
|
567
610
|
|
|
568
611
|
def _open_direct_via_signal_file(
|
|
569
|
-
filepath: str,
|
|
612
|
+
filepath: str,
|
|
613
|
+
title: str | None = None,
|
|
614
|
+
extra_args: list[str] | None = None,
|
|
570
615
|
) -> bool:
|
|
571
616
|
"""Write a direct-mode signal file for the VS Code extension.
|
|
572
617
|
|
|
@@ -574,6 +619,10 @@ def _open_direct_via_signal_file(
|
|
|
574
619
|
subprocess (``python -m arrayview --mode stdio <filepath>``) and hosts
|
|
575
620
|
the viewer HTML directly in a webview panel with postMessage transport.
|
|
576
621
|
No ports, no WebSocket, no authentication — just IPC.
|
|
622
|
+
|
|
623
|
+
*extra_args* are forwarded verbatim to the subprocess command line so
|
|
624
|
+
that new CLI flags (``--vectorfield``, ``--overlay``, ``--rgb``, …)
|
|
625
|
+
work without touching the extension.
|
|
577
626
|
"""
|
|
578
627
|
import sys as _sys
|
|
579
628
|
|
|
@@ -586,6 +635,8 @@ def _open_direct_via_signal_file(
|
|
|
586
635
|
}
|
|
587
636
|
if title:
|
|
588
637
|
payload["title"] = title
|
|
638
|
+
if extra_args:
|
|
639
|
+
payload["extraArgs"] = extra_args
|
|
589
640
|
return _write_vscode_signal(payload, skip_compat=True)
|
|
590
641
|
|
|
591
642
|
|
|
Binary file
|
|
@@ -74,9 +74,10 @@ const EXT_PPIDS = _getAncestorPids(process.pid, 8);
|
|
|
74
74
|
// bridges length-prefixed binary responses from stdout / JSON requests on stdin.
|
|
75
75
|
// ---------------------------------------------------------------------------
|
|
76
76
|
class PythonBridge {
|
|
77
|
-
constructor(filePath, onSessionReady, pythonPath, shmParams) {
|
|
77
|
+
constructor(filePath, onSessionReady, pythonPath, shmParams, extraArgs) {
|
|
78
78
|
this.filePath = filePath;
|
|
79
79
|
this.shmParams = shmParams || null; // {name, shape, dtype}
|
|
80
|
+
this.extraArgs = extraArgs || []; // additional CLI args (e.g. --vectorfield)
|
|
80
81
|
this.process = null;
|
|
81
82
|
this.sid = null;
|
|
82
83
|
this._buffer = Buffer.alloc(0);
|
|
@@ -89,38 +90,85 @@ class PythonBridge {
|
|
|
89
90
|
return new Promise((resolve, reject) => {
|
|
90
91
|
this._startResolve = resolve;
|
|
91
92
|
this._startReject = reject;
|
|
92
|
-
|
|
93
|
+
// Candidate order:
|
|
94
|
+
// 1. Explicit pythonPath (from signal file — always correct)
|
|
95
|
+
// 2. Workspace .venv (covers uv sync / editable installs during dev)
|
|
96
|
+
// 3. System python3/python (might have arrayview via pip)
|
|
97
|
+
// 4. uv run --with arrayview (ephemeral env from PyPI — always works)
|
|
98
|
+
if (this.pythonPath) {
|
|
99
|
+
this._candidates = [this.pythonPath];
|
|
100
|
+
} else {
|
|
101
|
+
this._candidates = ['python3', 'python', 'uv run --with arrayview python'];
|
|
102
|
+
// Prepend workspace .venv if it exists
|
|
103
|
+
const folders = vscode.workspace.workspaceFolders;
|
|
104
|
+
if (folders) {
|
|
105
|
+
for (const f of folders) {
|
|
106
|
+
const venvPy = path.join(f.uri.fsPath, '.venv', 'bin', 'python');
|
|
107
|
+
if (fs.existsSync(venvPy)) {
|
|
108
|
+
this._candidates.unshift(venvPy);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
this._tryNextCandidate();
|
|
93
115
|
});
|
|
94
116
|
}
|
|
95
117
|
|
|
118
|
+
_tryNextCandidate() {
|
|
119
|
+
if (this._candidates.length === 0) {
|
|
120
|
+
const msg = 'Python with arrayview not found. Install with: pip install arrayview (or uv pip install arrayview)';
|
|
121
|
+
log(`PYTHON: all candidates exhausted`);
|
|
122
|
+
if (this._startReject) { this._startReject(new Error(msg)); this._startReject = null; }
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const cmd = this._candidates.shift();
|
|
126
|
+
this._spawn(cmd);
|
|
127
|
+
}
|
|
128
|
+
|
|
96
129
|
_spawn(cmd) {
|
|
97
130
|
const { spawn } = require('child_process');
|
|
98
|
-
let
|
|
131
|
+
let arrayviewArgs;
|
|
99
132
|
if (this.shmParams) {
|
|
100
|
-
|
|
133
|
+
arrayviewArgs = ['-m', 'arrayview', '--mode', 'stdio',
|
|
101
134
|
'--shm-name', this.shmParams.name,
|
|
102
135
|
'--shm-shape', this.shmParams.shape,
|
|
103
136
|
'--shm-dtype', this.shmParams.dtype];
|
|
104
137
|
if (this.shmParams.arrayName) {
|
|
105
|
-
|
|
138
|
+
arrayviewArgs.push('--name', this.shmParams.arrayName);
|
|
106
139
|
}
|
|
107
140
|
} else {
|
|
108
|
-
|
|
141
|
+
arrayviewArgs = ['-m', 'arrayview', '--mode', 'stdio', this.filePath];
|
|
142
|
+
}
|
|
143
|
+
if (this.extraArgs.length > 0) {
|
|
144
|
+
arrayviewArgs.push(...this.extraArgs);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Support compound commands like "uv run python"
|
|
148
|
+
let spawnCmd, spawnArgs;
|
|
149
|
+
const parts = cmd.split(/\s+/);
|
|
150
|
+
if (parts.length > 1) {
|
|
151
|
+
spawnCmd = parts[0];
|
|
152
|
+
spawnArgs = [...parts.slice(1), ...arrayviewArgs];
|
|
153
|
+
} else {
|
|
154
|
+
spawnCmd = cmd;
|
|
155
|
+
spawnArgs = arrayviewArgs;
|
|
109
156
|
}
|
|
110
|
-
|
|
111
|
-
|
|
157
|
+
|
|
158
|
+
log(`PYTHON: spawning ${spawnCmd} ${spawnArgs.join(' ')}`);
|
|
159
|
+
this.process = spawn(spawnCmd, spawnArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
160
|
+
|
|
161
|
+
this._currentCmd = cmd;
|
|
112
162
|
|
|
113
163
|
this.process.on('error', (err) => {
|
|
114
|
-
if (err.code === 'ENOENT' &&
|
|
115
|
-
log(
|
|
116
|
-
this.
|
|
164
|
+
if (err.code === 'ENOENT' && this._candidates.length > 0) {
|
|
165
|
+
log(`PYTHON: ${spawnCmd} not found, trying next candidate`);
|
|
166
|
+
this._tryNextCandidate();
|
|
117
167
|
return;
|
|
118
168
|
}
|
|
119
169
|
log(`PYTHON: spawn error: ${err.message}`);
|
|
120
170
|
if (this._startReject) {
|
|
121
|
-
this._startReject(new Error(
|
|
122
|
-
cmd === 'python' ? 'Python not found. Install Python and arrayview (pip install arrayview).' : err.message
|
|
123
|
-
));
|
|
171
|
+
this._startReject(new Error(err.message));
|
|
124
172
|
this._startReject = null;
|
|
125
173
|
}
|
|
126
174
|
});
|
|
@@ -133,6 +181,7 @@ class PythonBridge {
|
|
|
133
181
|
try {
|
|
134
182
|
const info = JSON.parse(line.slice(8));
|
|
135
183
|
this.sid = info.sid;
|
|
184
|
+
this.sessionInfo = info; // preserve full session info (overlay_sid, etc.)
|
|
136
185
|
if (this.onSessionReady) this.onSessionReady(info);
|
|
137
186
|
if (this._startResolve) {
|
|
138
187
|
this._startResolve(info);
|
|
@@ -152,7 +201,13 @@ class PythonBridge {
|
|
|
152
201
|
});
|
|
153
202
|
|
|
154
203
|
this.process.on('exit', (code) => {
|
|
155
|
-
log(`PYTHON: exited with code ${code}`);
|
|
204
|
+
log(`PYTHON: ${this._currentCmd} exited with code ${code}`);
|
|
205
|
+
// If we haven't started yet and there are more candidates, try next
|
|
206
|
+
if (code !== 0 && this._startResolve && this._candidates && this._candidates.length > 0) {
|
|
207
|
+
log(`PYTHON: trying next candidate after exit code ${code}`);
|
|
208
|
+
this._tryNextCandidate();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
156
211
|
// Reject any pending request callbacks
|
|
157
212
|
for (const cb of this._pendingCallbacks) {
|
|
158
213
|
cb(Buffer.from(JSON.stringify({ error: `process exited with code ${code}` })));
|
|
@@ -202,36 +257,28 @@ class PythonBridge {
|
|
|
202
257
|
// ---------------------------------------------------------------------------
|
|
203
258
|
// Direct webview: embeds the viewer HTML directly (no iframe/server)
|
|
204
259
|
// ---------------------------------------------------------------------------
|
|
205
|
-
async function openDirectWebview(filePath, title, pythonPath, shmParams) {
|
|
206
|
-
const label = title || (filePath ? path.basename(filePath) : (shmParams && shmParams.arrayName) || 'Array');
|
|
207
260
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
'arrayview.viewer',
|
|
214
|
-
`ArrayView: ${label}`,
|
|
215
|
-
{ viewColumn, preserveFocus: false },
|
|
216
|
-
{
|
|
217
|
-
enableScripts: true,
|
|
218
|
-
retainContextWhenHidden: true,
|
|
219
|
-
}
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
// Spawn Python subprocess and wait for session to be ready
|
|
261
|
+
/**
|
|
262
|
+
* Wire up a webview panel to a PythonBridge: set HTML, bridge messages,
|
|
263
|
+
* clean up on dispose. Shared by openDirectWebview and the custom editor.
|
|
264
|
+
*/
|
|
265
|
+
async function setupArrayViewPanel(panel, filePath, pythonPath, shmParams, extraArgs) {
|
|
223
266
|
const bridge = new PythonBridge(filePath, (sessionInfo) => {
|
|
224
267
|
log(`SESSION READY: sid=${sessionInfo.sid} name=${sessionInfo.name}`);
|
|
225
|
-
}, pythonPath, shmParams);
|
|
268
|
+
}, pythonPath, shmParams, extraArgs);
|
|
226
269
|
const sessionInfo = await Promise.race([
|
|
227
270
|
bridge.start(),
|
|
228
271
|
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout waiting for Python session (30s)')), 30000)),
|
|
229
272
|
]);
|
|
230
273
|
|
|
231
|
-
// Get the rendered viewer HTML from the Python subprocess
|
|
274
|
+
// Get the rendered viewer HTML from the Python subprocess.
|
|
275
|
+
// Forward the full sessionInfo so the stdio server can build the correct
|
|
276
|
+
// query string (overlay_sid, etc.) without the extension needing to know
|
|
277
|
+
// about each feature.
|
|
232
278
|
const htmlPayload = await bridge.sendRequest({
|
|
233
279
|
type: 'get-viewer-html',
|
|
234
280
|
sid: bridge.sid,
|
|
281
|
+
...(bridge.sessionInfo || {}),
|
|
235
282
|
});
|
|
236
283
|
|
|
237
284
|
let viewerHtml;
|
|
@@ -239,13 +286,10 @@ async function openDirectWebview(filePath, title, pythonPath, shmParams) {
|
|
|
239
286
|
const response = JSON.parse(htmlPayload.toString());
|
|
240
287
|
viewerHtml = response.html;
|
|
241
288
|
} catch (e) {
|
|
242
|
-
vscode.window.showErrorMessage(`ArrayView: Failed to get viewer HTML: ${e.message}`);
|
|
243
289
|
bridge.destroy();
|
|
244
|
-
|
|
245
|
-
return;
|
|
290
|
+
throw new Error(`Failed to get viewer HTML: ${e.message}`);
|
|
246
291
|
}
|
|
247
292
|
|
|
248
|
-
// Set the webview HTML
|
|
249
293
|
panel.webview.html = viewerHtml;
|
|
250
294
|
|
|
251
295
|
// Bridge messages between webview and Python subprocess
|
|
@@ -383,9 +427,66 @@ async function openDirectWebview(filePath, title, pythonPath, shmParams) {
|
|
|
383
427
|
}
|
|
384
428
|
});
|
|
385
429
|
|
|
430
|
+
log(`DIRECT: setup complete for ${filePath}`);
|
|
431
|
+
return bridge;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function openDirectWebview(filePath, title, pythonPath, shmParams, extraArgs) {
|
|
435
|
+
const label = title || (filePath ? `ArrayView: ${path.basename(filePath)}` : (shmParams && shmParams.arrayName) || 'Array');
|
|
436
|
+
|
|
437
|
+
const viewColumn = vscode.window.activeTextEditor
|
|
438
|
+
? vscode.ViewColumn.Beside
|
|
439
|
+
: vscode.ViewColumn.Active;
|
|
440
|
+
|
|
441
|
+
const panel = vscode.window.createWebviewPanel(
|
|
442
|
+
'arrayview.viewer',
|
|
443
|
+
label,
|
|
444
|
+
{ viewColumn, preserveFocus: false },
|
|
445
|
+
{
|
|
446
|
+
enableScripts: true,
|
|
447
|
+
retainContextWhenHidden: true,
|
|
448
|
+
}
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
await setupArrayViewPanel(panel, filePath, pythonPath, shmParams, extraArgs);
|
|
453
|
+
} catch (e) {
|
|
454
|
+
panel.dispose();
|
|
455
|
+
throw e;
|
|
456
|
+
}
|
|
386
457
|
log(`DIRECT: opened "${label}" for ${filePath}`);
|
|
387
458
|
}
|
|
388
459
|
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
// Custom editor provider: "Open With..." from Explorer
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
class ArrayViewEditorProvider {
|
|
464
|
+
static viewType = 'arrayview.arrayEditor';
|
|
465
|
+
|
|
466
|
+
openCustomDocument(uri, _openContext, _token) {
|
|
467
|
+
// Minimal document — just wraps the URI. No data loading here;
|
|
468
|
+
// PythonBridge handles everything in resolveCustomEditor.
|
|
469
|
+
return { uri, dispose: () => {} };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async resolveCustomEditor(document, webviewPanel, _token) {
|
|
473
|
+
const filePath = document.uri.fsPath;
|
|
474
|
+
log(`CUSTOM-EDITOR: resolveCustomEditor for ${filePath}`);
|
|
475
|
+
webviewPanel.webview.options = { enableScripts: true };
|
|
476
|
+
webviewPanel.webview.html = `<html><body style="background:#1e1e1e;color:#ccc;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;font-family:system-ui">
|
|
477
|
+
<div>Loading ${path.basename(filePath)}…</div></body></html>`;
|
|
478
|
+
try {
|
|
479
|
+
await setupArrayViewPanel(webviewPanel, filePath);
|
|
480
|
+
log(`CUSTOM-EDITOR: setup complete for ${filePath}`);
|
|
481
|
+
} catch (e) {
|
|
482
|
+
log(`CUSTOM-EDITOR: error: ${e.message}\n${e.stack}`);
|
|
483
|
+
webviewPanel.webview.html = `<html><body style="color:#c00;padding:2em;font-family:monospace">
|
|
484
|
+
<h2>ArrayView failed to open</h2><pre>${e.message}</pre>
|
|
485
|
+
<p>Check Output → "ArrayView" for details.</p></body></html>`;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
389
490
|
let version = 'unknown';
|
|
390
491
|
let isProcessingSignal = false;
|
|
391
492
|
let lastHandledRequestId = null;
|
|
@@ -715,7 +816,10 @@ async function processSignalData(data) {
|
|
|
715
816
|
try { fs.unlinkSync(path.join(SIGNAL_DIR, compat)); } catch (_) {}
|
|
716
817
|
}
|
|
717
818
|
const shmParams = data.shm ? { ...data.shm, arrayName: data.arrayName } : null;
|
|
718
|
-
|
|
819
|
+
// Forward extra CLI args generically — Python builds this list from
|
|
820
|
+
// its argparse namespace so new flags work without extension changes.
|
|
821
|
+
const extraArgs = Array.isArray(data.extraArgs) ? data.extraArgs : [];
|
|
822
|
+
await openDirectWebview(data.filepath, data.title, data.pythonPath, shmParams, extraArgs);
|
|
719
823
|
return;
|
|
720
824
|
}
|
|
721
825
|
|
|
@@ -901,6 +1005,14 @@ function activate(context) {
|
|
|
901
1005
|
});
|
|
902
1006
|
context.subscriptions.push(openFileCmd);
|
|
903
1007
|
|
|
1008
|
+
// Register custom editor provider for "Open With..." support
|
|
1009
|
+
const editorProvider = vscode.window.registerCustomEditorProvider(
|
|
1010
|
+
ArrayViewEditorProvider.viewType,
|
|
1011
|
+
new ArrayViewEditorProvider(),
|
|
1012
|
+
{ webviewOptions: { retainContextWhenHidden: true } }
|
|
1013
|
+
);
|
|
1014
|
+
context.subscriptions.push(editorProvider);
|
|
1015
|
+
|
|
904
1016
|
// Start SSH relay listener for zero-config remote array viewing
|
|
905
1017
|
startRelayServer();
|
|
906
1018
|
context.subscriptions.push({ dispose: () => {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "arrayview-opener",
|
|
3
|
+
"displayName": "ArrayView Opener",
|
|
4
|
+
"description": "Opens ArrayView in VS Code and can promote tunnel ports to public preview",
|
|
5
|
+
"version": "0.13.0",
|
|
6
|
+
"publisher": "arrayview",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"icon": "icon.png",
|
|
9
|
+
"engines": { "vscode": "^1.80.0" },
|
|
10
|
+
"extensionKind": ["workspace", "ui"],
|
|
11
|
+
"categories": ["Other"],
|
|
12
|
+
"activationEvents": ["*"],
|
|
13
|
+
"main": "./extension.js",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/oscarvanderheide/arrayview.git"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"extension.js",
|
|
20
|
+
"package.json",
|
|
21
|
+
"icon.png",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"contributes": {
|
|
25
|
+
"commands": [
|
|
26
|
+
{
|
|
27
|
+
"command": "arrayview.openFile",
|
|
28
|
+
"title": "Open in ArrayView"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"customEditors": [
|
|
32
|
+
{
|
|
33
|
+
"viewType": "arrayview.arrayEditor",
|
|
34
|
+
"displayName": "ArrayView",
|
|
35
|
+
"selector": [
|
|
36
|
+
{ "filenamePattern": "*.npy" },
|
|
37
|
+
{ "filenamePattern": "*.npz" },
|
|
38
|
+
{ "filenamePattern": "*.nii" },
|
|
39
|
+
{ "filenamePattern": "*.nii.gz" },
|
|
40
|
+
{ "filenamePattern": "*.h5" },
|
|
41
|
+
{ "filenamePattern": "*.hdf5" },
|
|
42
|
+
{ "filenamePattern": "*.mat" },
|
|
43
|
+
{ "filenamePattern": "*.tif" },
|
|
44
|
+
{ "filenamePattern": "*.tiff" },
|
|
45
|
+
{ "filenamePattern": "*.pt" },
|
|
46
|
+
{ "filenamePattern": "*.pth" },
|
|
47
|
+
{ "filenamePattern": "*.zarr.zip" }
|
|
48
|
+
],
|
|
49
|
+
"priority": "default"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
Binary file
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "arrayview-opener",
|
|
3
|
-
"displayName": "ArrayView Opener",
|
|
4
|
-
"description": "Opens ArrayView in VS Code and can promote tunnel ports to public preview",
|
|
5
|
-
"version": "0.11.0",
|
|
6
|
-
"publisher": "arrayview",
|
|
7
|
-
"license": "MIT",
|
|
8
|
-
"engines": { "vscode": "^1.80.0" },
|
|
9
|
-
"extensionKind": ["workspace", "ui"],
|
|
10
|
-
"categories": ["Other"],
|
|
11
|
-
"activationEvents": ["*"],
|
|
12
|
-
"main": "./extension.js",
|
|
13
|
-
"repository": {
|
|
14
|
-
"type": "git",
|
|
15
|
-
"url": "https://github.com/oscarvanderheide/arrayview.git"
|
|
16
|
-
},
|
|
17
|
-
"files": [
|
|
18
|
-
"extension.js",
|
|
19
|
-
"package.json",
|
|
20
|
-
"LICENSE"
|
|
21
|
-
],
|
|
22
|
-
"contributes": {
|
|
23
|
-
"commands": [
|
|
24
|
-
{
|
|
25
|
-
"command": "arrayview.openFile",
|
|
26
|
-
"title": "ArrayView: Open File"
|
|
27
|
-
}
|
|
28
|
-
]
|
|
29
|
-
}
|
|
30
|
-
}
|
|
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
|