Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c70ddf5
FIX Inkscape version detection when diagnostic messages precede versi…
Copilot May 7, 2026
dba5b9a
FIX Vertex objects without NaNs rendering as fully transparent (#626)…
mvdoc May 9, 2026
0c050bb
FIX dataset dropdown auto-resizes to fit long dataset names (#630)
mvdoc May 9, 2026
ce27e8f
MNT cancel stale PR runs and improve Playwright cache reuse (#633)
mvdoc May 9, 2026
6c37dd7
FIX VertexRGB/VolumeRGB alpha attenuation in WebGL viewer (#632)
mvdoc May 10, 2026
28129b3
FIX WebGL viewer hover/click readout for Vertex2D / Volume2D (#634) (…
mvdoc May 10, 2026
d7449f9
TST harden headless tests against CI timeouts (#639)
mvdoc May 11, 2026
a10f381
FIX DataView.loaded race causing blank canvas in multi-data viewers (…
mvdoc May 11, 2026
74ff2f6
TST mock download_subject test to improve flakyness (#640)
mvdoc May 12, 2026
6fe15c3
replace np.in1d with np.isin (#641)
candytaco May 19, 2026
2c64b0b
webgl: fix ROI/sulci/label overlay toggles racing the texture bake (#…
jackgallant Jun 1, 2026
ab156df
webgl: redraw when the SVG overlay finishes its async texture bake (#…
jackgallant Jun 1, 2026
bd90468
Revert "webgl: redraw when the SVG overlay finishes its async texture…
jackgallant Jun 1, 2026
5be8a71
Revert "webgl: fix ROI/sulci/label overlay toggles racing the texture…
jackgallant Jun 1, 2026
833be76
Bump actions/checkout from 6 to 7 (#650)
dependabot[bot] Jun 23, 2026
6cb21e7
Bump codecov/codecov-action from 6 to 7 (#648)
dependabot[bot] Jun 23, 2026
9312115
FIX: update BuWtRd and BuWtRd_alpha colormaps to have pure white at c…
marklescroart Jun 23, 2026
ca97a36
Replace surf2surf matrix with direct pure-python nnfr construction
mvdoc Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/build_docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ on:
- main
workflow_dispatch: # Manual trigger for publishing docs

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
build-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0 # Required for setuptools-scm to get version from tags

Expand Down Expand Up @@ -48,6 +52,8 @@ jobs:
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-playwright-

- name: Install Playwright Chromium
run: playwright install --with-deps --only-shell chromium
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/codespell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ on:
pull_request:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read

Expand All @@ -17,6 +21,6 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Codespell
uses: codespell-project/actions-codespell@v2
6 changes: 5 additions & 1 deletion .github/workflows/install_from_wheel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ on:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
install-from-wheel:
runs-on: ubuntu-latest
Expand All @@ -17,7 +21,7 @@ jobs:
max-parallel: 5

steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
contents: read # Required to checkout the repository

steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0 # Required for setuptools-scm to get version from tags

Expand Down
12 changes: 9 additions & 3 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ on:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
run-tests:
runs-on: ubuntu-latest
Expand All @@ -17,7 +21,7 @@ jobs:
max-parallel: 5

steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
with:
Expand Down Expand Up @@ -47,16 +51,18 @@ jobs:
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-playwright-

- name: Install Playwright Chromium
run: playwright install --with-deps --only-shell chromium

- name: Test with pytest
timeout-minutes: 20 # in case webviewer tests hang
timeout-minutes: 25 # in case webviewer tests hang
run: pytest --cov=./

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@v7
with:
env_vars: OS,PYTHON
fail_ci_if_error: true
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,7 @@ docs/colormaps.rst

# Git worktrees
.worktrees

# Claude Code working directory (per-worktree scratch: launchers, verify
# scripts, plans). Not part of the project.
.claude
42 changes: 41 additions & 1 deletion cortex/dataset/braindata.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import hashlib
import warnings
from copy import deepcopy
import sys
from typing import Generic, Optional, TypeVar, Union, cast
Expand Down Expand Up @@ -552,7 +553,33 @@ def right(self):
def blend_curvature(self, alpha, threshold=0, brightness=0.5,
contrast=0.25, smooth=20):
"""Blend the data with a curvature map depending on a transparency map.


.. deprecated::
Per-vertex/voxel alpha is now honored directly by both the WebGL
viewer and ``cortex.quickshow``, so this curvature-blending hack
is no longer needed. The recommended replacement for scalar data
with a transparency map is :class:`Vertex2D` (or
:class:`Volume2D`) with a 2D colormap whose second axis encodes
alpha (e.g. ``"fire_alpha"``, ``"PU_RdBu_covar_alpha"``)::

# Was:
# blended = vtx.blend_curvature(alpha)
# cortex.quickshow(blended)
# Now:
v2d = cortex.Vertex2D(vtx.data, alpha, subject,
cmap="fire_alpha",
vmin=vtx.vmin, vmax=vtx.vmax,
vmin2=0, vmax2=1)
cortex.quickshow(v2d) # or cortex.webgl.show(v2d)

The 2D colormap path keeps colormap parameters (``cmap``,
``vmin``, ``vmax``) editable on the resulting object, and the
curvature underlay is composited through automatically by both
the matplotlib and WebGL renderers.

For data that is already RGB, pass ``alpha=`` to
:class:`VertexRGB` / :class:`VolumeRGB` directly instead.

Vertex objects cannot use transparency as Volume objects. This method
is a hack to mimic the transparency of Volume objects, blending the
Vertex data with a curvature map. This method returns a VertexRGB
Expand All @@ -577,6 +604,19 @@ def blend_curvature(self, alpha, threshold=0, brightness=0.5,
blended : VertexRGB object
The original map blended with a curvature map.
"""
warnings.warn(
"blend_curvature is deprecated and will be removed in a future "
"release. Per-vertex/voxel alpha is now honored directly by both "
"the WebGL viewer and quickshow, so this curvature-blending hack "
"is no longer needed. For scalar data with a transparency map, "
"use Vertex2D / Volume2D with a 2D colormap whose second axis "
"encodes alpha (e.g. 'fire_alpha', 'PU_RdBu_covar_alpha'), e.g. "
"`Vertex2D(data, alpha, subject, cmap='fire_alpha', vmin=..., "
"vmax=..., vmin2=0, vmax2=1)`. For data that is already RGB, "
"pass `alpha=` to VertexRGB / VolumeRGB directly.",
DeprecationWarning,
stacklevel=2,
)
from .views import Vertex
from .viewRGB import VertexRGB
# prepare curvature map
Expand Down
50 changes: 50 additions & 0 deletions cortex/export/headless.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import contextlib
import logging
import threading
import time
from typing import Any, Mapping, Optional

import cortex
Expand All @@ -50,6 +51,49 @@
logger = logging.getLogger(__name__)


def _wait_for_viewer_loaded(handle, timeout: float = 60.0) -> None:
"""Block until ``window.viewer.loaded`` resolves in the browser.

The WebSocket "connect" message fires as soon as the page DOM is ready,
but the WebGL viewer's CTM mesh download, parse, and initial render
typically take a few more seconds. ``viewer.loaded`` is a jQuery
Deferred that resolves at the end of ``setData`` (mriview.js), so polling
its ``.state()`` is a faithful "the viewer is ready to be driven"
signal that replaces brittle fixed sleeps in callers and tests.

We bypass the JSProxy attribute machinery and call ``send`` directly so
each poll is exactly one WebSocket roundtrip (the JSProxy ``__getattr__``
path would issue an extra ``query`` call per poll).
"""
deadline = time.monotonic() + timeout
poll_interval = 0.1
last_err: Optional[str] = None
while time.monotonic() < deadline:
try:
result = handle.send(
method="run", params=["window.viewer.loaded.state", []]
)
except Exception as exc:
last_err = repr(exc)
result = None
# WebApp.send always wraps its single per-client response in a list;
# unpack the leaf so the "resolved"/"pending"/error-dict check below
# is straightforward and last_err carries the actual value seen.
val = result[0] if isinstance(result, list) and result else result
if val == "resolved":
return
if val is not None:
last_err = (
str(val.get("error", val)) if isinstance(val, dict) else str(val)
)
time.sleep(poll_interval)
raise RuntimeError(
f"Viewer's .loaded deferred did not resolve within {timeout:.0f}s "
f"(last response: {last_err!r}). The CTM mesh may have failed to "
"download or parse, or mriview.js failed to initialise."
)


# --------------------------------------------------------------------------- #
# Helper: run Playwright in a dedicated thread to avoid asyncio conflicts #
# --------------------------------------------------------------------------- #
Expand Down Expand Up @@ -337,6 +381,12 @@ def headless_viewer(
# any point during the session (each call returns a fresh snapshot).
handle._pw_thread = pw_thread

# Block until the WebGL viewer has finished initialising (CTM mesh
# download + parse + first setData). Replaces ad-hoc time.sleep(10)
# calls in tests and callers, and shortens the wait when the
# browser is faster than the worst-case timeout.
_wait_for_viewer_loaded(handle, timeout=timeout)

yield handle

finally:
Expand Down
8 changes: 6 additions & 2 deletions cortex/export/save_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,12 @@ def save_3d_views(
cm = contextlib.nullcontext(cortex.webshow(volume, **viewer_params))

with cm as handle:
# Wait for the viewer to be loaded
time.sleep(sleep)
# Wait for the viewer to be loaded. The headless context manager
# already blocks on ``viewer.loaded`` before yielding, so we only
# need this fixed sleep for the interactive (real-browser) path
# where the user is opening the page manually.
if not headless:
time.sleep(sleep)

# Add interpolation and layers params only if we have a volume
if isinstance(volume, (cortex.Volume, cortex.Volume2D, cortex.VolumeRGB)):
Expand Down
Loading
Loading