Skip to content

Measurement markers in the waveform viewer; mT/m by default (v0.4.1) - #29

Merged
bughht merged 7 commits into
mainfrom
feature/measurement-markers
Oct 23, 2026
Merged

bughht merged 7 commits into
mainfrom
feature/measurement-markers

Conversation

@bughht

@bughht bughht commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds measurement markers to the waveform viewer. Right-click a waveform (long-press on touch) and choose Add marker to drop a vertical marker. Each marker shows every channel's value where it crosses, Δt to its neighbours, and its row in a table under the plot. Placement snaps to gradient corners and block boundaries as the pointer approaches.

It also makes mT/m the default gradient unit in every host, Python included, and lets the GitHub Pages preview host several feature branches at once. Released as v0.4.1.

Live preview of this branch: https://bughht.github.io/seqeyes_plugin/preview/measurement-markers/

What it does

  1. Markers (any number, labelled M1, M2, …)

    • Right-click → Add marker at Gx flat-top start (snapped) or Add marker at pointer (no snap).
    • Drag a marker to move it. This doesn't pan the view. Hold Shift to place it off the snap points.
    • To remove, right-click next to a marker, or use the × at the start of its table row, or Clear. Esc closes the menu or cancels a drag.
    • A long-press opens the menu on touch screens, and a finger drags a marker.
    • Markers are cleared when another sequence loads.
  2. Snapping

    • Targets:
      • trapezoid ramp and flat-top corners;
      • extended-trapezoid breakpoints and the bends of arbitrary gradients (found by simplifying the waveform);
      • RF start, centre and end;
      • ADC start, centre and end;
      • trigger starts;
      • block boundaries.
    • Coincident targets share one label, with the pointer's own row first (e.g. Gx ramp-down end · block #12/#13).
    • A ring marks the snap point as the pointer approaches. It appears only once markers are in use, so ordinary browsing is unchanged. Snapping can be turned off from the menu, and that choice is remembered.
  3. Readouts

    • On the plot: a dot and value where each marker crosses RF and Gx/Gy/Gz, and Δt brackets between neighbouring markers.
    • In the table:
      • × first, then the marker name, time, Δt, block and time within the block;
      • Gx/Gy/Gz in the current unit;
      • RF amplitude and phase, ADC sample and trigger;
      • M1/PNS while those rows are shown.
    • Copy gives tab-separated text. The table's scrollbars follow the theme.
    • Values read from the reduced whole-sequence overview of long arbitrary waveforms are marked ≈ until zoomed-in detail loads. Trapezoid corners, RF/ADC edges and block boundaries are always exact.
  4. mT/m by default in the web app, VS Code, MATLAB and the Python package:

    • In the viewers, a unit someone has already chosen is still restored.
    • In Python, grad_disp defaults to "mT/m" in SeqEyesViewer, render_html and the patched Sequence.plot, and unsupported units fall back to it. Passing grad_disp="Hz/m" still works.

How it fits the two renderers

The viewer is drawn by two copies of the renderer: the bundle (VS Code) and web/index.html's own copy (web app, MATLAB). Rather than writing the feature twice:

  • One module: markers.js (SeqEyesMarkers) is installed by each renderer with accessors to its own state. The last install wins, and the DOM is wired once.
  • Event handling: the module owns its listeners in the capture phase on #cc. Grabbing a marker therefore pre-empts both renderers' pan handlers, and neither copy's mouse or touch code changed.
  • Renderer changes: each renderer adds one line to its overlay pass and a reset on each load path.

Transport

packSequenceBlocks now also sends:

  • rf.c: the RF centre, which TE and TR are measured from.
  • r: 1 on waveforms whose samples the overview reduced. It is written only when set, and exact detail windows never carry it.

Preview workflow

preview-pages.yml now builds main at the root plus every branch in PREVIEW_BRANCHES under /preview/<branch>/, so one preview no longer wipes another. Notes:

  • On main this workflow does nothing on its own; it runs for pushes to listed branches or by hand.
  • feature/measurement-markers was added to the github-pages environment's deployment branch policy. That entry can be removed after merging.

Testing

  • Unit: 17 new tests in test/editor/markers.test.ts (corner detection, snapping and ranking, readouts with and without detail, the marker list and table), plus a transport test for rf.c and r.
  • Browser: 8 new tests in test/browser/markers.spec.ts:
    • snap to a known corner to 1e-12 s, Δt, no-snap placement;
    • Esc and outside click, overlay pixels, zoom, a new file;
    • remove and clear, drag with and without Shift, the remembered setting;
    • the M1 column, touch long-press and drag.
  • VS Code renderer: 1 new test in vscode-webview.spec.ts.
  • Changed: the persisted-settings test now picks G/cm, since choosing the default proved nothing. The Python default-unit tests now expect mT/m.
  • Python: pytest passes (27 passed, 1 skipped).
  • Full suites: npm run check (687 unit tests), all 111 browser tests and npm run test:vscode pass. CI is green for every commit.
  • Live preview: checked by driving it in a browser. Two markers on the demo GRE's readout corners give Δt = 3.2000 ms, exactly 128 × 25 µs.
  • VS Code 1.85.2: checked in a real window. Right-click shows the marker menu, not VS Code's, and adding a marker fills the table.

Files changed

  • src/editor/webview/assets/markers.js (new)
  • src/editor/webview/assets/drawing.js, state.js, interaction.js, prefs.js, prefs-ui.js, styles.css, template.html
  • web/index.html
  • src/editor/blockTransport.ts
  • scripts/build-webview.mjs
  • .github/workflows/preview-pages.yml
  • README.md (Features, Usage, and a new "Measurement markers" section)
  • Tests: test/editor/markers.test.ts (new), test/browser/markers.spec.ts (new), test/pulseq/block-transport.test.ts, test/browser/vscode-webview.spec.ts, test/browser/persisted-settings.spec.ts
  • Python: python/src/seqeyes/_plot.py, _renderer.py, resources/viewer.html, python/tests/test_plot.py, python/examples/demo.ipynb
  • Release v0.4.1: package.json, package-lock.json, python/pyproject.toml, python/src/seqeyes/__init__.py, matlab/toolboxInfo.xml
  • Generated bundles: web/*-bundle.js, web/styles.css, python/src/seqeyes/resources/*-bundle.js

🤖 Generated with Claude Code

bughht and others added 7 commits October 19, 2026 19:41
The preview workflow served one branch, feature/simulation, at a fixed path.
Each run replaced the whole site, so a second branch could not be previewed
without wiping the first.

It now builds main at the root and every branch in PREVIEW_BRANCHES under
/preview/<branch without "feature/">/, each stamped with its commit in
preview.txt. The branch that triggered the run is validated with
`npm run check`. main and the other listed branches are only rebuilt, since
their own pushes validate them. A run from a branch that is not listed fails
before building anything.

This branch, feature/measurement-markers, is the first entry. It is also in
the github-pages environment's deployment branch policy. A deploy from main
still removes the previews; re-run this workflow to restore them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Measurement markers snap to RF centres and read waveform values at any
time, so the viewer needs two facts the transport dropped:

- `rf.c`, the pulse's effective centre (absolute), already computed by the
  decoder. TE and TR are measured from it.
- `r: 1` on a gradient or RF waveform whose samples the overview reduced.
  Values interpolated across dropped samples are only approximate, and a
  reduced waveform's interior bends are not reliable snap targets. The flag
  is written only when set, to keep the JSON envelope small; exact detail
  windows never carry it.

Both hosts use this packer (the web app calls it in the page), so neither
needs another change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Right-click the waveform viewer and choose Add marker to drop a vertical
marker; add as many as needed (M1, M2, …).

- Snapping. Near a gradient corner the menu offers that point:
  - a trapezoid's ramp and flat-top corners;
  - an extended trapezoid's breakpoints, or an arbitrary gradient's bends
    (found by simplifying the waveform and keeping flat↔ramp changes,
    reversals, steps and large slope changes);
  - RF start, centre and end; ADC start, centre and end; trigger starts;
  - block boundaries.
  Coincident targets share one label, with the pointer's own row first. Once
  markers are in use, a ring marks the snap point as the pointer approaches.
  "Add marker at pointer (no snap)", or Shift while dragging, places one
  exactly; a remembered checkbox turns snapping off.
- Readouts. Each marker crosses the RF and gradient rows with a dot and its
  value, and Δt is drawn between neighbours. A table strip under the plot
  lists time, block and time within it, Δt, Gx/Gy/Gz in the current unit,
  RF amplitude and phase, the ADC sample and the trigger. Copy gives
  tab-separated text. Values read from a reduced overview are marked ≈.
- Editing. Drag to move (without panning the view), right-click next to a
  marker to remove it, or use the table. Esc closes the menu or cancels a
  drag. Markers are cleared when another sequence loads.

The viewer has two renderers: the bundle (VS Code) and web/index.html's own
copy (web app, preview, MATLAB). So the feature is one module, markers.js,
that each renderer installs with accessors to its own state. The last install
wins, and the module owns its listeners on #cc. A capture-phase mousedown
there claims a marker drag before either renderer's pan handler sees it, so
neither copy's mouse code changed. Each renderer adds one line to its
overlay pass and one reset per load path.

Tests:
- unit tests for corner detection, snapping and ranking, readouts with and
  without detail, the marker list and the table;
- browser tests in the web app for add, snap, Δt, no-snap, Esc and
  outside-click, overlay pixels, zoom, a new file, remove and clear, drag
  with and without Shift, and the remembered setting;
- one test through the VS Code renderer.

Also corrects a comment from the simulation work: the Python viewer has its
own renderer and does not load this bundle.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Touch:
- A long-press (half a second, without moving) opens the marker menu. The
  finger's later moves and lift are swallowed, so the view does not pan and
  the tap tooltip does not appear.
- A finger on a marker drags it, again without panning; snapping reaches
  twice as far as with a mouse.
- A plain tap still shows the readout, as before.

Like the mouse handling, this runs in the capture phase on #cc, so neither
renderer's touch code changed.

The table gains an "M1 / PNS" column while those rows are shown. It takes
each renderer's own hover-readout lines, so the values match the tooltip and
the plot (including coarse bands), and the column is in the copied TSV.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r, default to mT/m

- The marker table's × moves from the last column to the first, so it stays
  in reach when the table is wider than the window. Cells now carry
  data-col, and alignment uses classes rather than column positions, which
  shift when the M1/PNS column appears.
- The table's scrollbars use the theme's colours (thumb --ax, hovered --lb,
  on the strip's --trbg), via ::-webkit-scrollbar in Chromium, VS Code and
  Safari, and scrollbar-color elsewhere. They replace the platform's light
  grey bar, which stood out in dark themes.
- Gradients read in mT/m by default in the web app, VS Code and MATLAB. A
  unit someone has already chosen is still restored. The Python viewer's
  grad_disp default is part of its API and is unchanged.

The persistence test now picks G/cm, since choosing the default proved
nothing, and the opt-out test expects the new default.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The web app, VS Code and MATLAB viewers now open in mT/m, so the Python
package follows: `grad_disp` defaults to "mT/m" in `SeqEyesViewer`,
`render_html` and the patched `Sequence.plot`. An unsupported unit falls
back to it, and the bundled viewer template selects it. Passing
grad_disp="Hz/m" still works as before. The demo notebook's parameter table
says so (it also listed Hz/m twice).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Bump every surface carrying a version: package.json, both root package-lock
entries, python/pyproject.toml, the Python __version__,
matlab/toolboxInfo.xml, and the generated pulseq bundles. The lock entries
are edited by line, so a dependency sharing the version is not rewritten.

This release adds measurement markers to the waveform viewer and makes mT/m
the default gradient unit in every host.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bughht bughht changed the title Add measurement markers to the waveform viewer; default to mT/m Measurement markers in the waveform viewer; mT/m by default (v0.4.1) Oct 23, 2026
@bughht
bughht merged commit 09d93e5 into main Oct 23, 2026
17 checks passed

This branch was successfully deployed

1 active deployment
github-pages — 24854622 Deployed Oct 23, 2026 by bughht via build-and-deploy #24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant