apeGmsh Results Viewer¶
[!note] Companion document This file is the design charter for the post-solve viewer — what
results.viewer()opens, how it consumes the [[Results_architecture|Results]] module, and how it renders the seven topology levels (nodes, elements, line stations, gauss points, fibers, layers, springs).Pre-solve review (geometry, mesh, constraints, loads, masses) is covered by
MeshViewer/ModelViewerand is documented in [[apeGmsh_visualization]]. Pre- and post-solve are deliberately two viewers — same Qt+PyVista stack, same shared infrastructure underviewers/{scene,core,ui,overlays}, but different jobs and separately evolved.This document is the directives layer. The implementation phasing lives in
internal_docs/plan_results_viewer.md(to follow).
1. Scope¶
1.1 What the results viewer is¶
A Qt + PyVista desktop viewer that opens against a [[Results_architecture|Results]]
file (native .h5 or STKO .mpco) and renders post-solve quantities
on top of the bound FEMData mesh. Opened by:
results = Results.from_native("run.h5")
results.viewer() # blocks until closed
results.viewer(blocking=False) # spawns subprocess
results.viewer() is the only results viewer entry point. The
matching fem.viewer() (pre-solve) opens MeshViewer; the two are
parallel, not unified.
1.2 What it is not¶
- Not a notebook-inline tool. Use
g.model.preview()/g.mesh.preview()(Plotly) or matplotlib viag.plotfor inline figures. The results viewer needs Qt; for Colab / headless runs, produce static figures or VTU exports. - Not a solver-agnostic file viewer. It consumes
Resultsobjects. Reading raw.vtu/.pvdis dropped from the viewer surface (those remain export targets —results.export.vtu(...)). - Not the future scale-out path. The frozen sibling package
apeGmshViewer/is reserved for a future WebGL / Rust rewrite. See §10.
1.3 Phase boundary¶
| Concern | Pre-solve (MeshViewer) |
Post-solve (ResultsViewer) |
|---|---|---|
| Substrate | live FEMData |
bound FEMData from Results.fem |
| Visual subjects | mesh, constraints, loads, masses | result slabs (7 topology levels) + diagrams |
| Time | static | scrubber, play/pause, animation |
| Stages / modes | n/a | first-class — stage selector, modal browser |
| State mutation | edits PGs, picks selections | read-only on data; mutates view state |
| Entry | g.mesh.viewer(fem=...) |
results.viewer() |
Mesh-resolved overlays from the pre-solve viewer (constraint markers,
load arrows, mass spheres) do not appear in the results viewer.
A user who needs both reviews their model first with g.mesh.viewer,
runs the analysis, opens results.viewer. They are sequential tools.
2. The data shape¶
The viewer renders seven topology levels from
[[Results_architecture|Results]] §Layer 5. Each level has a slab type,
a natural rendering, and a selector grammar identical to FEMData's
(pg= / label= / selection= / ids=). The seventh, springs, was
added in Phase 11d / 11e.
| Topology | Slab type | values shape |
Natural rendering(s) |
|---|---|---|---|
| Nodes | NodeSlab |
(T, N) |
volume / surface contour, deformed shape, vector glyphs |
| Elements | ElementSlab |
(T, E, npe) |
per-element-node force arrows, color-per-cell |
| Line stations | LineStationSlab |
(T, sum_S) |
classic beam moment / shear / axial diagrams (hatched fill) |
| Gauss points | GaussSlab |
(T, sum_GP) |
sphere markers at GPs and interpolated continuum contour |
| Fibers | FiberSlab |
(T, sum_F) |
2-D side-panel section plot and 3-D dot cloud |
| Layers | LayerSlab |
(T, sum_L) |
shell mid-surface contour and through-thickness sub-panel |
| Springs | SpringSlab |
(T, E) |
force arrow along spring direction |
The viewer's renderer never sees raw Results reads — every slab is
fetched through the composite API (results.nodes.get(...),
results.elements.line_stations.get(...), etc.) so that lazy h5py
reads, partition stitching, and (class_tag, int_rule) grouping all
stay in the Results layer.
3. The Diagram abstraction¶
3.1 Why a Diagram is the right unit¶
Each result topology has a different rendering — beam diagrams are not
"color the mesh," fiber sections need a side panel, springs are
zero-length. A monolithic set_scalar_field() does not generalize.
What does: a Diagram — a stateful object that takes a slab spec and
the bound FEMData, produces VTK actors, and updates them on time
change.
Diagrams are first-class. They compose: showing stress contour on solids + bending moment on beams + fiber section on a picked column is three diagrams, not three special cases. Each has its own selector, its own colormap, its own scale, its own visibility toggle.
3.2 Diagram protocol¶
class Diagram: # composite-like (stateful)
"""A renderable layer driven by one or more Results slabs.
Subclasses implement attach/update/detach. Constructed against a
Results scoped to one stage; reads slabs through the composite API
and keeps lazy h5py handles open via the bound reader.
"""
id: str # unique within DiagramRegistry
kind: str # see catalogue below
style: DiagramStyle # frozen dataclass — render params
selector: SlabSelector # frozen dataclass — pg/label/ids/...
def attach(self, plotter, fem) -> None:
"""Build initial actors at step 0, register with plotter."""
def update_to_step(self, step_index: int) -> None:
"""Refresh actors for a new step. Bounded mutation; no re-attach."""
def detach(self) -> None:
"""Remove actors and release any cached arrays."""
def settings_widget(self):
"""Return a Qt widget for the per-diagram settings tab."""
DiagramStyle is per-diagram-type (frozen dataclass record) — a
ContourStyle is not the same as a LineForceStyle. SlabSelector is
shared:
@dataclass(frozen=True)
class SlabSelector:
pg: str | tuple[str, ...] | None = None
label: str | tuple[str, ...] | None = None
selection: str | tuple[str, ...] | None = None
ids: tuple[int, ...] | None = None
component: str = "" # canonical name, never shorthand
The viewer never holds a slab in RAM longer than one step's render. On time change, each diagram reads its slab for the new step (lazy h5py) and mutates its actors in place. This keeps memory bounded for million- DOF, multi-thousand-step files.
3.3 Diagram catalogue¶
Eight diagrams, one per natural rendering of the data shapes in §2. The implementation phasing is in the plan; this is the target catalogue.
| Diagram | Slab(s) consumed | Renders |
|---|---|---|
ContourDiagram |
NodeSlab (point data) or GaussSlab (cell data when n_gp == 1, point data via shape-fn extrapolation when n_gp > 1) |
Colored substrate slice, scalar bar, optional deformed warp |
DeformedShapeDiagram |
NodeSlab (translational + rotational components) |
Warped mesh + undeformed reference + scale slider; optional contour overlay |
VectorGlyphDiagram |
NodeSlab (3 components) |
Arrows at nodes, colored / scaled by magnitude |
LineForceDiagram |
LineStationSlab |
Hatched fill perpendicular to beam axis (axial / shear / moment / torsion); textbook engineering style |
FiberSectionDiagram |
FiberSlab |
3-D dot cloud at section locations + side panel: 2-D section plot, dots colored by fiber stress / strain |
LayerStackDiagram |
LayerSlab |
Shell mid-surface contour + side panel: through-thickness profile at picked GP |
SpringForceDiagram |
SpringSlab |
Force arrow along the configured spring direction at zero-length elements |
GaussPointDiagram |
GaussSlab |
Sphere markers at natural→global GP locations, colored by component value |
LoadsDiagram |
fem.nodes.loads.by_pattern(...) (broker, not a Results slab) |
Constant force arrows at loaded nodes, per pattern; not step-resolved (timeSeries info absent) |
ReactionsDiagram |
NodeSlab (reaction_force_* + reaction_moment_*) |
Straight arrows for forces, curved-arrow glyphs for moments at constrained nodes; auto-filtered by per-node max |
Two intentional non-diagrams:
- Per-element nodal force arrows (
ElementSlabwithnodal_resisting_force_*components) collapse intoVectorGlyphDiagramdriven off the per-element-node slab. Same visual; different data wiring. - Mode shapes are not a separate diagram — they are
DeformedShapeDiagramopened on a stage withkind="mode". The Stage panel detects mode stages and offers an animation toggle.
3.3a ContourDiagram — three rendering paths¶
ContourDiagram is the most-used diagram in the catalogue and the only
one that fans into more than one substrate path. It supports both nodal
data (the bread-and-butter case for displacements and reactions) and
element-level Gauss data (stresses, strains, plastic work). The path is
chosen at attach time after a single step-0 read used both for the
n_gp probe and the initial scatter.
The user-facing knob is ContourStyle.topology with three values:
"auto"(default) — prefer nodal data when both composites have the requested component; fall through to Gauss otherwise."nodes"— force the nodal-scalar path."gauss"— force the Gauss path; cell-vs-node sub-decision is made internally based onn_gp.
The three internal paths:
| Path | Trigger | Substrate slice | Per-step write |
|---|---|---|---|
nodes |
topology="nodes" (or "auto" when component is in nodes only) |
extract_points(node_indices) |
Scatter into point_data["_contour"] |
gauss_cell |
topology="gauss" and every selected element has n_gp == 1 (CST / tri31, hex8 with one-point integration) |
extract_cells(cell_indices) |
Scatter into cell_data["_contour"] |
gauss_node |
topology="gauss" and at least one selected element has n_gp > 1 (tet10, hex20/27, etc.) |
extract_points over the union of corner nodes touched by the slab |
Per-step extrapolation → scatter into point_data["_contour"] |
The gauss_node path is the only one that does non-trivial work
between the slab read and the scatter — the others are direct
mappings. The extrapolation pipeline lives in
[apeGmsh.results._gauss_extrapolation] and works as follows:
- Per-element extrapolation matrix. Evaluate the element's shape
functions at the slab's GP natural coords to get
Ashape(n_gp, n_corner); computeM = pinv(A)shape(n_corner, n_gp). For square systems (e.g. hex8 + 2×2×2 GPs) this is the exact inverse and constant + linear fields are reproduced bit-for-bit. - Linear-counterpart projection. For higher-order types (tri6 / quad8/9 / tet10 / hex20/27) the matrix uses the linear counterpart's shape functions (tri3 / quad4 / tet4 / hex8) rather than the full higher-order N matrix. Two reasons:
- The substrate is built from linear cells.
build_fem_scenedrops mid-side / face / center nodes, so non-corner extrapolations are never painted. pinvon the full higher-order N matrix yields a non-constant nodal field for a constant GP input (minimum-norm regularization of the under-determined system), which is the wrong behaviour for visualization.- Per-element apply.
nodal[t, c] = M @ gp_values[t, :]for each selected element. - Cross-element averaging. Per-element nodal contributions accumulate into a global per-node sum + count; the final value is the mean across neighbouring elements.
Smoothing across element boundaries is the price of a single-mesh
nodal contour. Standard post-processor behaviour (STKO, ParaView,
most academic viewers). A future per-element subdivision path can
preserve discontinuities at material interfaces — out of scope today;
the gauss_cell cell-data scaffolding stays in place to keep that
door open.
3.4 What a Diagram is not¶
- Not a renderer. It does not own a
Plotter. It receives one inattach(). - Not session-aware. It takes a scoped
Results(a stage) plus a selector. It never importsgmsh.*. - Not pickle-serializable. State is the actor handles + cached arrays
for the current step. Persistence is via
DiagramSpec(record — frozen dataclass capturing(kind, style, selector)), saved / loaded by the Director.
3.5 Stack-neutrality¶
Diagrams take slabs and a selector, not a pre-rendered VTK mesh.
The transformation slab + fem -> actors lives entirely inside the
diagram. A future WebGLRenderer swap rewrites the renderer adapter,
not the diagram catalogue. This is the cheapest forward-compatibility
guarantee we can buy without paying for a full IR layer today.
4. ResultsDirector — single source of truth¶
4.1 What the Director owns¶
ResultsDirector (composite)
├── results — bound Results (stage-scoped)
├── stage_id — active stage id
├── step_index — active time index
├── time_mode — single | range | envelope | animation
├── registry — DiagramRegistry (ordered list of Diagrams)
├── on_step_changed — observer
├── on_stage_changed — observer
└── on_diagrams_changed — observer
The Director is the only thing that knows what step / stage is
active. Diagrams are passive — they receive update_to_step(i) calls
when the Director's step changes. UI tabs subscribe to Director
observers and fire user actions back through Director methods.
4.2 Time modes¶
| Mode | Step semantics | Use case |
|---|---|---|
single |
One step rendered | Scrub the timeline, pick a moment |
range |
A step range — diagrams render envelope | "Max stress over the dynamic stage" |
envelope |
Full stage — same as range over [0, n_steps) |
Capacity / demand checks |
animation |
Auto-advance step at fixed FPS | Visual review of a transient or mode |
range / envelope modes change the slab read: each diagram's slab
becomes (T, ...) instead of (1, ...), the diagram reduces over
time (max / abs-max / signed-extreme), and the actor renders the
reduction. Implementation detail of each diagram.
4.3 Stage navigation¶
director.stage("gravity") # switch to a stage by name/id
director.step(40) # jump to step 40
director.set_time_mode("animation")
director.next_mode() # for kind="mode" stages
Stage change calls detach() on every diagram, re-binds them to the
new stage's scoped Results, and attach() again. This is more
expensive than step change; it happens rarely.
For modal browsing (kind="mode" stages), the Director exposes
results.modes as the source set. The Mode panel (UI) iterates over
them; each mode is its own scoped Results so step semantics collapse to
step_index=0, T=1.
4.4 Geometry → Composition → Diagram hierarchy¶
The diagram registry is wrapped by a three-level outline
(viewers/diagrams/_geometries.py:10-21):
GeometryManager ← director.geometries
└── Geometry ← outline first level
└── CompositionManager
└── Composition ← outline second level (UI: "Diagram")
└── Diagram ← layer (Contour, VectorGlyph, …)
A Geometry is a deformation-bearing container that owns a list
of compositions. Deformation is per-Geometry (field + scale) and
only the active Geometry renders — switching geometry re-applies that
geometry's deformation to the substrate and routes per-Geometry state
to its compositions. A Composition is what the user sees as
"Diagram" in the outline; it bundles one or more Layers (the
internal Diagram instances). Bootstrap is one always-present
"Geometry 1" with an empty composition manager; the manager refuses
to remove the last surviving geometry so the viewer always has
somewhere to land.
4.4a Per-Geometry display panel¶
When a Geometry row is selected in the outline, the Details dock
shows viewers/ui/_geometry_settings_panel.py with two sections:
Deformation (toggle / field / scale) and Display (show-mesh /
show-nodes toggles + a single opacity slider applied to substrate
fill, wireframe, and node cloud while the geometry is active). These
were global SessionPanel knobs before the geometry refactor; per-
Geometry now lets one view dim its substrate beneath a contour while
another keeps full alpha.
4.5 Event-loop dispatcher¶
viewers/diagrams/_dispatch.py is the single pipeline for the four
primitives that drive what the viewport paints:
- STEP — push current step values to one or all diagrams
(
Diagram.update_to_step(step_index)). - DEFORM — recompute deformed substrate points and call
Diagram.sync_substrate_pointson one or all diagrams; mutatesscene.grid.pointsin place when the scope is "all". - GATE — run the composition gate: each actor's visibility is
d.is_visible AND (no_active_comp OR id(d) in active_layers). - RENDER — single coalesced
plotter.render().
Every UI gesture, observer, and shortcut funnels through
Dispatcher.fire(event_kind, ...), which selects the right primitive
sequence from the event matrix (e.g. step_changed → STEP + DEFORM +
RENDER; comp_active_changed → GATE + RENDER). The
session_batch(...) context manager suppresses every primitive in
between and runs one full pump on exit, used during _apply_session
to kill the N-squared registry pump.
4.6 Observability¶
Every dispatch / gesture / pick / error funnels through
viewers/_log.py::log_action. Two destinations: stderr (INFO+, one
human-readable line per gesture) and a per-session file
(~/.apegmsh/viewer-logs/session-YYYYMMDD-HHMMSS.log, DEBUG+, full
action trail). Bug reports attach the most recent file and we replay
every gesture the user made.
5. Selection — what gets data¶
The viewer reuses the exact selection vocabulary from
[[Results_architecture|Results]] §Layer 5. Every diagram is constructed
with a SlabSelector. Two independent dials:
- What carries data — the diagram's selector. PGs, labels,
selection sets, raw IDs. Resolved through the bound
FEMData. - What is shown — the viewer's
VisibilityManager(reused fromviewers/core/visibility.py). Per-cell extraction; hidden geometry leaves no silhouette.
Concretely: a ContourDiagram selecting pg="Body" paints data on
Body's elements only, regardless of what's hidden. Hiding the slab via
the visibility manager makes those cells invisible; the data is still
there if revealed.
This separation matters for "show stress on the structure, hide the ground but keep the foundation contoured" — two diagrams (structure + foundation), one visibility filter (hide ground).
5.1 Pick-driven selection¶
The reused PickEngine + SelectionState (from viewers/core/) drive
two pick targets:
- Element / node pick — feeds the Inspector panel. Shows the element / node id, coordinates, all components currently bound by any diagram, and a one-click "create probe at this node" action.
- Pick-to-Diagram — pick an element, right-click → "Show line diagram for this beam" / "Open fiber section here." Constructs a Diagram pre-selecting the picked entity.
Box selection works the same way it does in MeshViewer — produces a
list of dim-tags or element IDs, stages them as a SlabSelector for a
new diagram.
6. UI surface¶
6.1 Window layout¶
Wraps viewers/ui/viewer_window.ViewerWindow in a Qt-native dock
arrangement (viewers/ui/_results_window.py). The viewport is the
immovable central widget; five QDockWidget panels surround it,
movable / floatable / tabifiable, with object names so layout state
round-trips through QSettings.
┌──────────────────────────────────────────────────────────────┐
│ (OS native title bar) │
├────────────┬─────────────────────────────┬───────────────────┤
│ Outline │ │ Plots │
│ (dock, │ 3D viewport │ Details │
│ left) │ (central widget) │ Session │
│ │ │ (docks, right) │
├────────────┴─────────────────────────────┴───────────────────┤
│ Time Scrubber (dock, bottom — Movable | Floatable, no close) │
└──────────────────────────────────────────────────────────────┘
The Time Scrubber dock is always visible because time is the
primary axis — it carries the step slider, time-value readout,
play / pause / step / loop controls, stage dropdown, and (for modal
files) the mode browser. Layout persists across launches under
QSettings('apeGmsh', 'ResultsViewer'); reset_layout() restores
the default arrangement captured at startup.
6.2 Docks¶
| Dock | Owns |
|---|---|
| Outline | Three-level tree — Geometries → Compositions ("Diagrams") → Layers; add / remove / reorder / toggle |
| Plots | Diagram-specific 2-D side panels (fiber section, layer through-thickness, time history) |
| Details | Context-sensitive editor: outline-selected Geometry / Composition / Layer settings, picked entity |
| Session | Theme picker, restore / save session, screenshot, view presets, per-session preferences |
| Time Scrubber | Step slider, time readout, play / pause / step / loop, stage dropdown, mode browser |
The Outline dock is the spine of the workflow — adding a layer opens a small dialog (kind, slab selector, component, stage, initial style) and the new layer appears under its Composition. Toggling its visibility mutes its actors without detaching; removing detaches and disposes. Per-layer / per-Geometry editors render into the Details dock when their outline row is selected.
6.2a Per-card Apply¶
Settings cards (in the Details dock) stage edits and commit them
on a per-card Apply button rather than emitting on every
keystroke. Pending value-edit appliers are collected during widget
construction and committed together when Apply is clicked
(viewers/ui/_diagram_settings_tab.py:378-379, 608). This kills the
mid-typing render storm and gives the user a clear commit point —
Cancel discards staged edits.
6.3 Diagram-specific side panels¶
Two diagram kinds need extra real estate:
FiberSectionDiagram— a docked side panel showing the 2-D section plot for the currently picked beam station. Re-renders on pick / time change. Backed by matplotlib-in-Qt (FigureCanvasQTAgg).LayerStackDiagram— a docked side panel showing the through- thickness profile (component vs_thickness) at the currently picked GP. Same matplotlib-in-Qt pattern.
Both panels live as dockable widgets so the user can detach them or arrange them alongside the 3-D viewport. Closing the diagram closes its panel.
7. Renderer integration¶
7.1 What we reuse from viewers/¶
The integrated viewer infrastructure already covers the lion's share of what a results viewer needs. Reuse, don't fork:
| Module | Used for |
|---|---|
viewers/scene/mesh_scene.py |
Substrate mesh — same Gmsh→VTK linearization, same dim batching |
viewers/scene/glyph_points.py |
Node cloud, gauss-point sphere glyphs |
viewers/core/entity_registry.py |
DimTag ↔ cell-index maps; pick resolution |
viewers/core/pick_engine.py |
Cell + box picking with modifier keys |
viewers/core/selection.py |
SelectionState for picked entities |
viewers/core/color_manager.py |
Hidden / picked / hovered / idle priority on the substrate mesh |
viewers/core/visibility.py |
Hide / isolate / reveal via extract_cells |
viewers/core/navigation.py |
Quaternion orbit, pan, zoom (camera bindings) |
viewers/ui/viewer_window.py |
QMainWindow shell, tab dock, toolbar |
viewers/ui/theme.py |
10 palettes + theme editor + custom-theme loader |
viewers/ui/preferences_manager.py |
26-field persistent preferences |
viewers/ui/preferences.py |
Session tab (theme, point size, line width, edges, AA) |
viewers/overlays/origin_markers_overlay.py |
World-origin marker overlay |
What ResultsViewer adds on top:
viewers/results_viewer.py— the top-level classviewers/diagrams/— the Diagram catalogueviewers/ui/results_tabs.py— Diagrams / Stages / Inspector tabsviewers/ui/_time_scrubber.py— bottom dockviewers/overlays/probe_overlay.py— mined fromapeGmshViewer/
7.2 Color manager and diagrams — boundary¶
The reused ColorManager owns per-cell RGB on the substrate mesh
(idle / hidden / picked / hovered). Diagrams render to separate
actors; they do not fight the substrate ColorManager.
A ContourDiagram builds its own colored actor on a slice of the
substrate mesh (extracted by selector). Idle substrate stays flat. This
is the same pattern overlays use today — additive, not overlapping.
7.3 Origin shift, scale¶
viewers/scene/mesh_scene.py applies a numerical-stability origin
shift to large-coordinate models. Diagrams must respect
registry.origin_shift when placing glyphs at coordinates derived from
slabs (e.g., GP world coords from natural coords + shape functions).
Same convention as the existing origin markers overlay.
8. Probes¶
The probe system from apeGmshViewer/visualization/probes.py is mined
into viewers/overlays/probe_overlay.py with one structural change:
probes consume Results slabs through the Director, not VTU
point_data dicts.
| Probe | Action | Result |
|---|---|---|
| Point | Click on the mesh; samples all bound diagrams at the closest mesh node | PointProbeResult — node id, distance, value-per-diagram |
| Line | Pick A and B endpoints; samples N points along the line | LineProbeResult — chainage, value-per-diagram |
| Plane | Drag an interactive plane widget to slice the mesh | PlaneProbeResult — slice mesh + scalars |
| Time series | Right-click → "plot history" on a picked node or element / GP | TimeHistoryResult — time-vs-value matplotlib chart |
The fourth (time series) is a new probe — apeGmshViewer/ had no
notion of time. It piggybacks on the lazy slab read: pulling a single
node's full (T,) displacement is one h5py call.
Probe results display in the Probes tab and stay alive across step / stage changes. A line probe seen during the gravity stage remains visible after switching to dynamic, with values updated.
9. Subprocess opt-in¶
results.viewer() defaults to blocking — same as MeshViewer and
ModelViewer. The notebook user adds %gui qt once, or accepts that
the viewer blocks until closed.
results.viewer(blocking=False) is the documented escape hatch:
- The Results file path is already known (the
Resultswas opened from disk). - The viewer launches as
python -m apeGmsh.viewers <path>in a subprocess. - The subprocess opens a fresh
Results.from_native(path)(orfrom_mpco), constructs aResultsViewer, and runs its own Qt event loop. - The notebook returns immediately. The viewer survives notebook / kernel crashes.
Implementation footprint: ~30 LOC. The subprocess machinery is the only concession to the notebook workflow — everything else is in-process.
If the Results was constructed in-memory (no file path — e.g., from a
domain-capture session that hasn't flushed), blocking=False raises
with a clear message: "In-memory Results cannot launch in a subprocess.
Either pass blocking=True or call results.flush(path) first."
10. The frozen apeGmshViewer/ sibling¶
The top-level apeGmshViewer/ package stays in the repository. It is:
- Frozen. No active development. Bug fixes only if it actively breaks under a dependency upgrade.
- Detached from the dispatch path.
results.viewer()no longer spawns it. Nothing insrc/apeGmsh/imports it. - A placeholder. When the time comes for a genuine WebGL / Rust rewrite (better large-dataset handling, browser-deployable, no Qt dependency), the work happens here. The Diagram catalogue from §3.3 is the spec; the renderer adapter is what changes.
The 4,444 LOC currently in apeGmshViewer/ is largely duplicated by
src/apeGmsh/viewers/. The mining ledger in §11 details what gets
salvaged for the integrated viewer; the rest stays where it is, for
the future rewrite to either keep or discard.
11. Mining ledger — what comes from apeGmshViewer/¶
Concrete file-by-file disposition. Quantified against the integrated viewer to make the duplication explicit.
11.1 Lift (refactored against Results)¶
| Source | LOC | Destination |
|---|---|---|
apeGmshViewer/visualization/probes.py |
605 | viewers/overlays/probe_overlay.py |
apeGmshViewer/panels/probe_panel.py |
260 | viewers/ui/_probes_tab.py |
Both files import apeGmsh.viewers.ui.theme already; the refactor
swaps MeshData.point_data["disp"] reads for Director.read_at_pick
calls into the bound Results.
11.2 Pattern lift (rebuilt against slabs)¶
| Source / pattern | What's kept | Rebuilt as |
|---|---|---|
renderer.show_deformed / create_deformed_mesh |
Idea: warp mesh by displacement, optional undeformed ref | diagrams/_deformed_shape.py |
renderer.show_vectors |
Idea: glyph orient/scale from a vector field | diagrams/_vector_glyph.py |
renderer.set_active_time_step |
Idea: replace mesh data on step change without re-build | Diagram.update_to_step contract |
controls.py (time slider region) |
Idea: scrubber + play/pause + step / loop | ui/_time_scrubber.py |
11.3 Discard¶
| Source | LOC | Reason |
|---|---|---|
apeGmshViewer/loaders/vtu_loader.py |
290 | Obsolete — Results is the canonical input |
apeGmshViewer/ui/theme.py |
895 | Older duplicate of viewers/ui/theme.py (1054 LOC) |
apeGmshViewer/ui/preferences.py |
212 | Older duplicate of viewers/ui/preferences_manager.py |
apeGmshViewer/main_window.py |
725 | Older duplicate Qt shell — viewers/ui/viewer_window.py is newer |
apeGmshViewer/panels/model_tree.py |
141 | Integrated _browser_tab.py + _parts_tree.py are richer |
apeGmshViewer/panels/controls.py (most) |
~150 | Colormap / opacity scaffolding — re-emit on integrated chrome |
apeGmshViewer/panels/properties.py |
131 | Replaced by Diagram-aware Inspector tab |
apeGmshViewer/visualization/renderer.py |
~400 | Most duplicates mesh_scene + color_manager + visibility |
apeGmshViewer/visualization/navigation.py |
9 | Trivially wraps the integrated install_navigation |
Total mined: ~865 LOC lifted, ~330 LOC of patterns rebuilt. Total discarded: ~3,000 LOC of duplicate infrastructure.
The discarded code stays inside apeGmshViewer/ (frozen package, §10).
It is not deleted; it is simply no longer on the integrated viewer's
import path.
12. File structure¶
Target layout under src/apeGmsh/viewers/ after the rebuild:
src/apeGmsh/viewers/
├── __init__.py
├── model_viewer.py (existing — pre-solve geometry)
├── mesh_viewer.py (existing — pre-solve mesh + overlays)
├── geom_transf_viewer.py (existing — Three.js beam-axis tool)
├── results_viewer.py NEW — top-level ResultsViewer
│
├── diagrams/ NEW package
│ ├── __init__.py
│ ├── _director.py ResultsDirector (composite)
│ ├── _registry.py DiagramRegistry (composite)
│ ├── _base.py Diagram base class + DiagramSpec record
│ ├── _selectors.py SlabSelector record + helpers
│ ├── _styles.py per-diagram DiagramStyle records (frozen dataclasses)
│ ├── _contour.py ContourDiagram
│ ├── _deformed_shape.py DeformedShapeDiagram
│ ├── _vector_glyph.py VectorGlyphDiagram
│ ├── _line_force.py LineForceDiagram
│ ├── _fiber_section.py FiberSectionDiagram
│ ├── _layer_stack.py LayerStackDiagram
│ ├── _spring_force.py SpringForceDiagram
│ └── _gauss_marker.py GaussPointDiagram
│
├── ui/
│ ├── results_tabs.py NEW — Diagrams / Stages / Inspector tabs
│ ├── _time_scrubber.py NEW — bottom dock
│ ├── _diagrams_tab.py NEW — list + add / remove / reorder
│ ├── _diagram_settings_tab.py NEW — per-diagram style editor
│ ├── _stages_tab.py NEW — stage list, mode browser
│ ├── _inspector_tab.py NEW — picked entity + values
│ ├── _probes_tab.py NEW — mined from apeGmshViewer
│ └── ... (existing — reused unchanged)
│
├── overlays/
│ ├── probe_overlay.py NEW — mined from apeGmshViewer
│ └── ... (existing — reused unchanged)
│
├── scene/ ── existing, reused unchanged
└── core/ ── existing, reused unchanged
Outside the viewers package:
src/apeGmsh/results/Results.py
└── viewer() REPLACED — was subprocess to apeGmshViewer;
now constructs ResultsViewer in-process
apeGmshViewer/ FROZEN — placeholder for future stack swap
13. Class-flavor inventory¶
Following [[apeGmsh_principles]] §5 tenet (ix):
| Class / module | File | Flavor |
|---|---|---|
ResultsViewer |
viewers/results_viewer.py |
composite |
ResultsDirector |
viewers/diagrams/_director.py |
composite |
DiagramRegistry |
viewers/diagrams/_registry.py |
composite |
Diagram (base) |
viewers/diagrams/_base.py |
composite |
ContourDiagram, … |
viewers/diagrams/_*.py |
composite |
DiagramSpec |
viewers/diagrams/_base.py |
record |
SlabSelector |
viewers/diagrams/_selectors.py |
record |
ContourStyle, … |
viewers/diagrams/_styles.py |
record (one per kind) |
PointProbeResult, … |
viewers/overlays/probe_overlay.py |
record |
| Tab widgets | viewers/ui/_*_tab.py |
def |
_time_scrubber |
viewers/ui/_time_scrubber.py |
def |
This matches the existing visualization surface — composites for state-bearing classes, frozen records for the typed data they pass around, defs for stateless UI factories.
14. Contributor rules¶
Six rules for adding to the results viewer surface, parallel to [[apeGmsh_visualization]] §6:
-
Diagrams take slabs and selectors, not pre-rendered VTK. The transformation
slab + fem -> actorslives inside the Diagram. A future renderer swap rewrites the actor side; the catalogue stays. -
Mutate state through the Director.
step_index,stage_id,time_mode,registryare the Director's. UI widgets call Director methods; they do not poke diagram internals. -
One slab per diagram per step. Lazy h5py reads are the budget constraint. A diagram that needs 5 components at once reads 5 slabs, releases them after
update_to_stepcompletes. No global slab cache. -
Diagrams subscribe to step changes; Director subscribes to UI. Observer chain is
UI → Director → Diagrams. Reverse calls are forbidden — a diagram does not calldirector.set_step(...). -
Reuse from
viewers/scene,core,ui,overlaysalways. The results viewer adds Diagrams and stage/time UI; it does not reimplement theme, preferences, navigation, picking, visibility, or color management. New shared infrastructure goes under those existing directories. -
Lazy-import Qt. Same rule as the rest of the viewer surface —
apeGmsh.results.Resultsmust remain importable on a headless kernel.Results.viewer()importsviewers.results_viewerinside the function body, never at module load.
15. Open items deferred to the plan¶
These are decisions that affect the implementation phases but do not
need to be settled at the directives level. They land in
internal_docs/plan_results_viewer.md:
- Diagram auto-suggestion. When a results file is opened, should
the Director propose a default diagram set (e.g., contour on
displacement_z, line forces on beams)? Or always start blank? - Style presets. A
DiagramStylelibrary (saved JSON, like custom themes) — let users save and re-apply favorite colormaps / clims. Useful but not v1. - Multi-pane synchronized views. Two viewports of the same model with different selectors / different stages. Defer.
- Animation export. Render a stage to a
.mp4/.gif. Useful but not v1. - Comparison mode. Open two
Resultsfiles side by side (e.g., before / after a parameter change). Touches Director internals; defer.
See also¶
- [[apeGmsh_visualization]] — the full visualization surface map (pre-solve viewers + viz/ inline tools)
- [[Results_architecture]] — Phase 9 explicitly defers this work to a separate plan; this document is the directives layer of that plan
- [[apeGmsh_principles]] — tenets (viii) "the viewer is core and environment-aware" and (ix) "three object flavors, three class styles"
src/apeGmsh/results/Results.py— the API the viewer consumessrc/apeGmsh/viewers/mesh_viewer.py— the parallel pre-solve viewer to crib structure from