Skip to content

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

Merged
jcschaff merged 9 commits into
masterfrom
feat/field-viewer-annotations
Aug 8, 2026
Merged

3D field viewer: deformed-grid convention, annotations, cut plane, picking, time plots, statistics (#1859 items B–E)#1861
jcschaff merged 9 commits into
masterfrom
feat/field-viewer-annotations

Conversation

@jcschaff

@jcschaff jcschaff commented Aug 7, 2026

Copy link
Copy Markdown
Member

Implements items B–E of #1859 on top of #1860 (merged): color bar, tick-labeled axes, the orthogonal cut plane, value-under-mouse picking, time-series at a picked point, and statistics. All viewer changes in webapp-viewer/; the field server (FieldViewerServer) gains /timeseries and /stats; the custom wasm bundle advances to v1.2.0.

The deformed-grid convention (the design center of this PR)

VCell's FV display convention: the solution lives on a regular Cartesian grid with an implicit boundary; for display, the boundary vertices of the volume mesh are smoothed (windowed sinc, reference parameters), yielding one unstructured grid whose boundary cells are distorted hexahedra reaching the smoothed surface. The viewer builds exactly that — raw boundary (vtkGeometryFilter, passThroughPointIds) → sinc → vtkVCellDeformGridToSurface (our custom write-back filter, vcell-vtk-wasm#2) — and derives everything from the one mesh: the shell is its boundary; the cut plane is vtkTableBasedClipDataSet on it; display-mesh statistics integrate over it. Geometry, mesh and statistics tell one story, at any smoothing setting (default = the reference parameters).

Design history, for the record: an earlier iteration smoothed only the extracted surface and patched the cut face against it (GPU clip + raw-grid cap + implicit-distance trim). Review correctly rejected it — the volume elements being cut were still right hexahedra, so the cut rim degenerated to a shaved staircase. That approach is fully removed; this section describes the only implementation in the PR.

Cut plane — a crop of the deformed grid

Axis dropdown + position slider. The cut face is flat and sharp (never smoothed — the order is deform-then-clip); its rim lies on the smoothed surface because the boundary cells reach it; every cut polygon carries its cell's exact value through the clip. At smoothing 0 the rim honestly reverts to the raw staircase; at nominal and above it follows the smoothed silhouette — verified at 48³ resolution against the exact shots that condemned the previous approach.

face-on Z crop, nominal smoothing X crop orbited toward the cut smoothing 0 — raw staircase, consistently
nominal orbit raw

Color bar and tick-labeled axes (items 2, 3)

vtkScalarBarActor + vtkCubeAxesActor via addViewProp (addActor2D is refused by the invoker), each with a default-on checkbox. The viewer owns one vtkLookupTable shared by surface and bar (useLookupTableScalarRangeOn) — the bar labels the LUT's range, which mapper.setScalarRange alone never reaches — with hue flipped to blue-low→red-high to match the desktop. Verified with a monotonic ramp field: surface sweep and bar direction/labels agree.

annotated scene ramp verification
annotated ramp

Picking and time series (items 6, 8)

Hover resolves in plain JS — occupancy map + 3D-DDA over the Cartesian lattice, no round trip, no picker object — honoring the crop's keep-rule (picking the cut face reads the cap cell). A press that moves < 4 px plots the picked cell's full time course via the new /timeseries endpoint, which maps the cell to the solver's global volume index and reduces server-side through the same VCDataManager.getTimeSeriesValues path as the desktop's own plots — the data crosses the wire once, already reduced. Known caveat: the pick walks the regular lattice, so within the boundary band it can disagree with the deformed geometry by less than a cell; noted for a follow-up.

pick and plot

Statistics — two documented families (item 9 + design discussion)

  • Solver-grid (/stats, Java, reader-side): one space-stats TimeSeriesJobSpec carrying all requested variables, each reduced over its own domain — uniform voxel volumes, the solver's own bookkeeping, serving the min/mean/max time curves (Stats button → mean curves + min–max envelopes, shared scale, legend). Fast; min/max exact; means carry the known full-voxel distortion near membranes.
  • Display-mesh (new, wasm): when a crop is active, a live readout shows min · volume-weighted mean · max · volume of the cropped region, integrated by vtkIntegrateAttributes over the deformed (clipped) mesh — self-consistent with the geometry on screen, labeled with family + smoothing parameters. Sanity: the cropped half-ellipsoid's volume matches the analytic value to 4 digits.

Min/max are identical between families by construction; the two means differ near membranes by design — the delta is a boundary-resolution diagnostic — and converge as the mesh refines. Documented in webapp-viewer/README.md.

stats plot

Bundle: vcell-vtk-wasm v1.1.0 → v1.2.0

  • v1.1.0 (#1): marshalled vtkClipDataSet/vtkTableBasedClipDataSet.
  • v1.2.0 (#2): adds vtkVCellDeformGridToSurface (the write-back must live in the bundle — the session has no bulk data readback) and marshals vtkCellSizeFilter/vtkIntegrateAttributes. Both released through the build + golden-test gate; the fetch script pins v1.2.0; probe.html (committed) verifies the roster on every bump.

Verification

All features exercised headless against a mock field server at 18³ and 48³ (screenshots above are real renders of this branch): annotations, ramp-field color consistency, crop across the smoothing range including the rim-fidelity comparison, picking (incl. crop-aware cap picks), time-series plot, stats curves, cropped-region stats with analytic volume check. The Java endpoints compile and ride the same data paths as the desktop's plots; live-client exercise still to come via the usual -Dvcell.fieldViewer.enabled=true route.

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

🤖 Generated with Claude Code

https://claude.ai/code/session_01FYqrC3BiB4JRsJoz7BXJF4

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
Redesign of the cut plane after design review: VCell's FV display
convention smooths the boundary vertices OF THE VOLUME MESH, yielding
one unstructured grid whose boundary cells are distorted hexahedra
reaching the smoothed surface. The viewer now builds exactly that —
raw boundary (vtkGeometryFilter, passThroughPointIds) → windowed sinc
→ vtkVCellDeformGridToSurface (our custom write-back filter, bundle
v1.2.0) — and derives everything from the one mesh: the shell is its
boundary, the cut plane is vtkTableBasedClipDataSet on it. The cut
face is flat and sharp, its rim lies on the smoothed surface because
the cells reach it, every cut polygon carries its cell's value, and
the rim tracks the smoothing slider from raw staircase (0) through
nominal to heavy — verified at 48^3 against the shots that condemned
the previous approach. The GPU clipping plane, cutter, implicit-
distance cap trim and second actor are all deleted.

First display-mesh statistic, per design discussion: when a crop is
active, min / volume-weighted mean / max / volume of the cropped
region, computed by vtkIntegrateAttributes over the deformed mesh and
labeled with family + smoothing parameters. (The mean divides ∫f dV by
the Volume array in JS — DivideAllCellDataByVolume is a plain set
macro whose boolean setter doesn't marshal.) Solver-grid /stats stays
the reference family: uniform voxel volumes, the solver's own
bookkeeping; min/max identical by construction, the mean delta is a
boundary-resolution diagnostic, both converge as h→0. Sanity check:
cropped half-ellipsoid volume matches the analytic value to 4 digits.

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: annotations, cut plane, picking, time plots, stats (#1859 items B–E) 3D field viewer: deformed-grid convention, annotations, cut plane, picking, time plots, statistics (#1859 items B–E) Aug 8, 2026
@jcschaff
jcschaff merged commit 8e5e0df into master Aug 8, 2026
9 checks passed
@jcschaff
jcschaff deleted the feat/field-viewer-annotations branch August 8, 2026 05:13
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