3D field viewer: annotations, cut plane, picking, time plots, stats (#1859 items B–E) - #1861
Open
jcschaff wants to merge 8 commits into
Open
3D field viewer: annotations, cut plane, picking, time plots, stats (#1859 items B–E)#1861jcschaff wants to merge 8 commits into
jcschaff wants to merge 8 commits into
Conversation
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
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
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
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
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 tomaster. All changes are inwebapp-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.setScalarRangenever reaches — with the mapper's implicit LUT the bar would read [0,1] under a correctly colored surface. The viewer now owns onevtkLookupTableshared 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 refusesaddActor2D(probed earlier, recorded inwebapp-viewer/README.md; this PR adds the LUT-range gotcha to those notes).Verification (live, headless harness against a mock field server)
0.251–0.997at t=1 →5.75e-5–0.969at t=0) and the t=0 all-blue boundary is physically correct for the mock's interior gaussian.visibilityOn/Off).is not permittedrefusals throughout — none.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 CAxis 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.
mapper.addClippingPlaneworks in this wasm build even though WebGL2 has nogl_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.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+vtkImplicitPolyDataDistancebuilt 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:vtkClipDataSetand both siblings have no registered constructor in this bundle (probed; recorded in the README along withprobe.html, the scratch probe page).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
/timeseriesendpoint. 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 sameVCDataManager.getTimeSeriesValuespath 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).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-statsTimeSeriesJobSpeccarrying all the variables — each reduced over its own domain's cells, since one run's variables can live on different geometry — through the samegetTimeSeriesValuespath 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.
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.htmlnow 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.