Skip to content

3D field viewer: annotations, cut plane, picking, time plots, stats (#1859 items B–E) - #1861

Open
jcschaff wants to merge 8 commits into
masterfrom
feat/field-viewer-annotations
Open

3D field viewer: annotations, cut plane, picking, time plots, stats (#1859 items B–E)#1861
jcschaff wants to merge 8 commits into
masterfrom
feat/field-viewer-annotations

Conversation

@jcschaff

@jcschaff jcschaff commented Aug 7, 2026

Copy link
Copy Markdown
Member

Implements items B and C of #1859: the two annotation actors and the orthogonal cut plane. Stacked on #1860 (base = feat/field-viewer-labels); when that merges, GitHub retargets this to master. All changes are in webapp-viewer/ — no Java.

Color bar (vtkScalarBarActor)

Slim strip on the right edge, titled with the variable name, with a checkbox to hide it.

The load-bearing detail: the scalar bar labels the lookup table's range, which mapper.setScalarRange never reaches — with the mapper's implicit LUT the bar would read [0,1] under a correctly colored surface. The viewer now owns one vtkLookupTable shared by mapper and bar (useLookupTableScalarRangeOn), re-ranged and rebuilt on every field change, so the two cannot disagree. Hue range flipped to blue-low → red-high to match the desktop results viewer (VTK's default rainbow runs the other way).

Axes box with ticks (vtkCubeAxesActor)

Bounds from the raw grid (the smoothed surface shrinks slightly), camera-attached so tick labels track rotation — the thing an HTML overlay could not fake, per the issue discussion. Checkbox to hide, default on.

Both actors enter the scene via renderer.addViewProp — the standalone session's invoker refuses addActor2D (probed earlier, recorded in webapp-viewer/README.md; this PR adds the LUT-range gotcha to those notes).

Verification (live, headless harness against a mock field server)

  • Monotonic ramp field: surface sweeps blue (x-min) → red (x-max) and the bar's direction and labels match the served range — proof mapper and bar share the LUT.
  • Time scrubbing: bar re-labels (0.251–0.997 at t=1 → 5.75e-5–0.969 at t=0) and the t=0 all-blue boundary is physically correct for the mock's interior gaussian.
  • Toggles: each checkbox hides its actor and re-renders cleanly (visibilityOn/Off).
  • Console watched for the invoker's silent is not permitted refusals throughout — none.
annotated scene ramp verification
annotated ramp

On merge, tick items (2), (3) and (4) in #1859.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FYqrC3BiB4JRsJoz7BXJF4


Orthogonal cut plane — a CROP, not a floating slice (vtkPlane + vtkCutter + mapper clipping) — item C

Axis dropdown (Off/X/Y/Z) + position slider with a coordinate readout. Axis-aligned only, per the issue's guidance — an interactive plane widget needs interactor infrastructure the standalone session does not have.

  • The volume is genuinely cropped: the surface mapper carries a GPU clipping plane that discards the half above the cut, and the cutter's cross-section caps the exposed face. The surface stays opaque — the first iteration's 25%-opacity ghost shell (review feedback: "the cropped portion isn't really cropped") is gone.
  • mapper.addClippingPlane works in this wasm build even though WebGL2 has no gl_ClipDistance — VTK emulates clipping in-shader. It is also a quiet API (accepted with nothing logged either way), so it was verified by same-camera on/off renders orbited toward the removed side.
  • The cap is cut from the raw grid — the cutter passes cell data through, so each cut polygon carries its source voxel's exact value, colored through the same shared lookup table as the surface and the bar.
  • Live re-cut while dragging, with the same in-flight guard as orbiting; a domain switch re-feeds the cutter and remaps the slider onto the new bounds. The slider insets slightly from the axis ends, where a plane on the outermost faces cuts a degenerate sliver; cuts landing exactly on interior voxel faces render fine.
face-on Z crop (cap where the front half was) X crop, orbited toward the cut same camera, crop off
z crop x crop off

Cap rim follows the smoothing slider (review feedback)

The order is smooth-then-cut: the cut face stays flat and sharp (the cap is planar by construction), while its in-plane outline is trimmed to the smoothed silhouette via vtkClipPolyData + vtkImplicitPolyDataDistance built from the sinc output. At slider 0 the rim is the raw staircase — consistent with the raw shell — and it smooths with the slider like every other view. Clip-the-grid-then-smooth would have rounded the cut edge off, and is unavailable anyway: vtkClipDataSet and both siblings have no registered constructor in this bundle (probed; recorded in the README along with probe.html, the scratch probe page).

smoothing 0 — staircase rim nominal — trimmed heavy (48 iters) — elliptical rim
rim raw rim nominal rim smooth

Value under the mouse + time-series at a picked point — item D (#1859 items 6 and 8)

(6) Hover readout — plain JS, no VTK picker, as the issue recommends: the browser holds the whole grid and field, so the pick is an occupancy map (built once per geometry) plus an Amanatides–Woo 3D-DDA walk along the mouse ray — a few dozen lattice steps per hover regardless of cell count, no round trip, no serialization-registry dependence. The walk applies the renderer's crop keep-rule, so hovering the exposed cut face reads the cap voxel, not the invisible removed half. Blanked (NaN) cells read "no data".

(8) Click → time course via a new /timeseries endpoint. The reduction happens server-side next to the reader, per the issue's explicit design point: the endpoint maps the viewer's cell index to the solver's global volume index through the served grid and goes through the same VCDataManager.getTimeSeriesValues path the desktop's own time plots use — one code path for local and remote runs, data travels once, already reduced. Client renders a framework-free inline-SVG polyline with min/max + time-range labels. A press that moves < 4 px is a pick; more is an orbit.

Verified in the mock harness: hover center reads the front-most voxel with correct value/coords, background hover clears, click produces the 5-point curve, and with the Z-crop active the same mouse position resolves to the cap voxel (9.5, 8.5, 8.5) instead of the removed half. The Java endpoint compiles; it exercises the same time-series path as the desktop plots (not exercised live in the harness, which mocks the server).

pick and plot

On merge, tick items (2), (3), (4), (6) and (8) in #1859.


Spatial min/mean/max over time — item E (#1859 item 9)

The strongest server-side-reduction case in the issue: computing these client-side would pull every timestep of every variable. New /stats?sim&job[&var=a,b,c] endpoint (default: all volume variables) runs one space-stats TimeSeriesJobSpec carrying all the variables — each reduced over its own domain's cells, since one run's variables can live on different geometry — through the same getTimeSeriesValues path as /timeseries. Weighted mean when the reader provides it, unweighted otherwise (identical on a uniform Cartesian grid).

Client: a Stats button renders every variable (capped at the palette size) into the shared plot panel — solid mean curve + translucent min-to-max envelope per variable on one common scale, with a color-swatch legend. Verified in the harness: the diffusing variable's mean rises as its envelope widens while the time-constant test field draws a flat mean with a constant band — physically correct on sight.

stats plot

Bundle bumped to vcell-vtk-wasm v1.1.0

virtualcell/vcell-vtk-wasm#1 (merged, built, released) marshals vtkClipDataSet / vtkTableBasedClipDataSet; the fetch script now pins v1.1.0. Probed against the released bundle: both clip filters construct (+setClipFunction +insideOutOn), no invoker refusals, and the full viewer pipeline renders unchanged. probe.html now covers them for future bumps.

On merge, tick items (2), (3), (4), (6), (8) and (9) in #1859 — leaving only (5) movie export and (7)/(1), which are already in #1860.

jcschaff and others added 2 commits August 7, 2026 15:25
vtkScalarBarActor and vtkCubeAxesActor, the two annotation actors from
#1859 item B, added through renderer.addViewProp (addActor2D is refused
by the standalone session's invoker). Each has a checkbox; both default
on.

The load-bearing detail is the lookup table: the scalar bar labels the
LUT's range, which mapper.setScalarRange never reaches, so with the
mapper's implicit table the bar would read [0,1] under a correctly
colored surface. The viewer now owns one vtkLookupTable shared by
mapper and bar (useLookupTableScalarRangeOn), re-ranged and rebuilt on
every field change, so the two cannot disagree. Hue range is flipped to
blue-low → red-high to match the desktop results viewer. Verified
against a monotonic ramp field: surface sweep, bar direction and bar
labels agree, and the labels track time scrubbing.

The axes box takes its bounds from the raw grid (not the smoothed
surface, which shrinks) and the camera, so its tick labels follow
rotation — the one thing an HTML overlay could not fake.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYqrC3BiB4JRsJoz7BXJF4
vtkPlane + vtkCutter (#1859 item C), driven by an axis dropdown and a
position slider — deliberately not an interactive plane widget, which
needs interactor infrastructure the standalone session does not have.

The cutter takes the raw unstructured grid, not the smoothed surface:
the point of a slice is the interior field, and the cutter passes cell
data through, so each cut polygon carries its source voxel's exact
value, colored through the same lookup table as the surface and the
color bar. While a slice is shown the boundary surface drops to 25%
opacity — opaque, it would hide the slice entirely.

The slider maps to the sliced axis's extent with a small inset at the
ends, where a plane on the outermost faces cuts a degenerate sliver;
cuts landing exactly on interior voxel faces render fine (verified).
Live-drag re-cutting uses the same in-flight guard as orbiting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYqrC3BiB4JRsJoz7BXJF4
@jcschaff jcschaff changed the title 3D field viewer: color bar and tick-labeled axes box (#1859 item B) 3D field viewer: color bar, tick-labeled axes, and orthogonal cut plane (#1859 items B + C) Aug 7, 2026
jcschaff and others added 2 commits August 7, 2026 16:43
Review feedback on the first cut: a cross-section floating inside a
25%-opacity shell reads as "nothing was actually cropped" — and it
wasn't. Now the surface mapper carries a GPU clipping plane that
genuinely discards the half above the cut, the cutter's cross-section
caps the exposed face, and the surface stays opaque; the opacity hack
is gone.

mapper.addClippingPlane works in this wasm build despite WebGL2 having
no gl_ClipDistance — VTK emulates clipping in-shader. It is also a
quiet API: the call is accepted with nothing logged either way, so this
was verified by comparing same-camera renders with the crop on and off,
orbited toward the REMOVED side. The first orbit looked at the kept
hemisphere, which looks whole from its own side by definition — a
false "clipping is a no-op" diagnosis worth remembering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYqrC3BiB4JRsJoz7BXJF4
Review feedback: the cap's outline was the raw voxel staircase while
the shell around it was smoothed, so the cut boundary ignored the
smoothing slider. Order of operations per review: smooth THEN cut —
the cut face must stay flat and sharp, never rounded off.

The shell already obeys that order (GPU clip of the sinc output). The
cap now does too: it remains the planar raw-grid cross-section (exact
per-voxel values, perfectly flat), and vtkClipPolyData carves its
in-plane outline against a vtkImplicitPolyDataDistance built from the
smoothed surface. The rim therefore tracks the slider exactly:
staircase at 0 (consistent with the raw shell), smooth at nominal and
above. The distance function is re-anchored only when the smoothed
surface changes — setInput rebuilds its locator — not per drag.

The clip-the-grid-then-smooth alternative would have rounded the cut
edge and is unavailable anyway: vtkClipDataSet and both siblings have
no registered constructor in this bundle (probed; recorded in the
README, along with probe.html, the scratch page used to find out).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYqrC3BiB4JRsJoz7BXJF4
jcschaff and others added 2 commits August 7, 2026 17:49
Item 6 of #1859, resolved entirely in plain JS as the issue suggests:
the browser already holds the whole grid and the field array, so a
hover costs no round trip, no picker object, and no dependence on the
serialization registry (vtkCellPicker stays an unused fallback).

The voxels are axis-aligned boxes on a regular Cartesian lattice, so
picking is an occupancy map built once per geometry plus an
Amanatides-Woo 3D-DDA walk along the mouse ray — a few dozen lattice
steps per hover, independent of cell count. The walk honors the active
crop with the renderer's own keep-rule, so hovering the exposed cut
face reads the voxel at the cap, not the invisible half in front of
it. Readout shows value and cell-center coordinates; blanked (NaN)
cells read "no data".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYqrC3BiB4JRsJoz7BXJF4
Item 8 of #1859. The important design point is server-side, per the
issue: do NOT fetch every timestep to the browser to build one curve.
The field server gains /timeseries?sim&job&domain&var&cell, which maps
the viewer's cell index to the solver's global volume index through
the served grid and reduces through the same
VCDataManager.getTimeSeriesValues path the desktop's own time plots
use — so local and remote runs share one code path, and the data
travels once, already reduced to a single point's course.

Client side: a click that never really moved (< 4 px) is a pick, not
an orbit; the picked cell comes from the same JS ray walk as the hover
readout, so the crop rule applies — clicking the exposed cut face
plots the cap voxel. The curve renders as a framework-free inline SVG
polyline with min/max and time-range labels and a close button.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYqrC3BiB4JRsJoz7BXJF4
@jcschaff jcschaff changed the title 3D field viewer: color bar, tick-labeled axes, and orthogonal cut plane (#1859 items B + C) 3D field viewer: color bar, axes, cut plane, picking + time plots (#1859 items B–D) Aug 7, 2026
jcschaff and others added 2 commits August 7, 2026 19:18
Item 9 of #1859, the strongest case for server-side reduction in the
issue: computing these curves client-side would mean pulling every
timestep of every selected variable. The field server gains
/stats?sim&job[&var=a,b,c] (default: all volume variables), which runs
ONE space-stats TimeSeriesJobSpec carrying all the variables — each
reduced over its own domain's cells, since variables in one run can
live on different geometry — through the same getTimeSeriesValues path
as /timeseries. Weighted mean when the reader provides it, unweighted
otherwise (identical on a uniform Cartesian grid).

Client: a Stats button renders all variables (capped at the palette
size) into the shared plot panel — solid mean curve plus a translucent
min-to-max envelope per variable, one common scale so curves compare
directly, and a color-swatch legend. The single-series and stats plots
share one axes/scale helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYqrC3BiB4JRsJoz7BXJF4
vcell-vtk-wasm v1.1.0 (patch 0002 there) registers constructors for
vtkClipDataSet and vtkTableBasedClipDataSet, closing the gap found in
the cut-plane review. Probed against the released bundle: both
construct with setClipFunction + insideOutOn, no invoker refusals, and
the full viewer pipeline renders unchanged on the new bundle.
probe.html now covers the clip filters so the next bundle bump
re-verifies them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYqrC3BiB4JRsJoz7BXJF4
@jcschaff jcschaff changed the title 3D field viewer: color bar, axes, cut plane, picking + time plots (#1859 items B–D) 3D field viewer: annotations, cut plane, picking, time plots, stats (#1859 items B–E) Aug 7, 2026
@jcschaff
jcschaff changed the base branch from feat/field-viewer-labels to master August 8, 2026 02:33
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