Interactive viewer

meshio++ can show you a mesh, through one entry point with two backends.
import meshioplusplus
mesh = meshioplusplus.read("part.msh")
meshioplusplus.view(mesh) # pick a backend automatically
meshioplusplus.view(mesh, backend="polyscope") # a native desktop window
meshioplusplus.view(mesh, backend="browser") # vtk.js, in a browser or notebookdesktop (polyscope) | browser (vtk.js) | |
|---|---|---|
| install | pip install meshioplusplus[viewer] | nothing extra |
| volume meshes | drawn as solids you can slice | drawn by their boundary |
| headless screenshots | yes | no |
| works in a notebook | no | yes, inline |
| needs a display | yes | no |
backend="auto" (the default) uses polyscope when it is installed and a display is available, and the browser otherwise — so the same call works over SSH, in a notebook, and on a workstation.
Desktop viewer (Polyscope)
Polyscope is an optional, Python-only dependency. It is never imported unless you ask for it, and it is not part of the C++/WASM/C/Fortran core.
pip install meshioplusplus[viewer]Without it, view(backend="polyscope") and screenshot() raise an error naming that command; everything else, including view(backend="browser"), works unchanged.

The bundled example.msh bracket, coloured by quality:scaled_jacobian — this image is generated by screenshot() itself.
meshioplusplus.view(mesh, color_by="temperature")
meshioplusplus.view(mesh, kind="surface") # boundary only: much lighter
meshioplusplus.view(mesh, show=False) # register without blockingScreenshots
screenshot() renders without opening a window, so it works over SSH, in CI and in a docs build:
meshioplusplus.screenshot(mesh, "part.png", color_by="quality:scaled_jacobian",
size=(1600, 1200))It needs EGL or a virtual framebuffer. On a headless Linux box either libegl1 or xvfb-run -a python … will do; the error names the backends it tried.
How a mesh becomes a picture
Polyscope's volume meshes hold tetrahedra and hexahedra only, so:
- tetrahedral and hexahedral meshes are drawn directly, keeping their elements;
- wedges, pyramids and the higher-order family go through
convert_cells(mode="simplexify")first; kind="surface"runsextract_surfaceand draws the boundary — much lighter for a large solid, and the cell data follows each facet's owning cell so a material or tag still colours correctly;- 2D meshes become surface meshes, keeping mixed triangle/quad and polygon blocks exactly as they are (no triangulation);
- 1D meshes become curve networks, and a mesh with no cells a point cloud.
point_data becomes vertex quantities and cell_data cell/face quantities. A 3-component array becomes a vector quantity plus a magnitude scalar; a 6- or 9-component one becomes per-component scalars plus a Frobenius norm. Three components are treated as a colour only when the array is named like one (color, rgb, *_color, *_rgb) — a normalized displacement field in [0, 1] is a vector, not an RGB triple.
Anything lossy is reported as a warning rather than done quietly:
Warning: view: volume cells (wedge) are not renderable directly and were split into tetrahedraBrowser viewer (vtk.js)
The hosted viewer reads any of the formats the WebAssembly build supports, and also works as a client-side format converter. Everything runs in your browser: there is no server and no upload, and a file you open never leaves your machine.
From Python, the same viewer renders a mesh you already have:
meshioplusplus.view(mesh, backend="browser") # opens your browser
meshioplusplus.view(mesh, backend="browser", inline=True) # inline in JupyterIn a Jupyter notebook this displays inline by default; elsewhere it writes a self-contained HTML file and opens it. That file needs no server and no network — the viewer is bundled in the wheel.
It draws surfaces
vtk.js has no unstructured-grid model at all: vtkPolyData is its only mesh type. So the browser backend renders VTP, and a volume mesh is shown by its boundary. That is not a choice the viewer made; it is what a surface renderer can draw. If you need to see inside a solid, use the desktop backend.
Boundary facets inherit their owning cell's data, so colouring a solid by its per-cell material or tag works — extract_surface alone would drop it.

A 4×4×4 hexahedral block: 64 cells in the file, 96 boundary quads on screen, coloured by the per-cell layer tag that the gather brought onto the surface.
Mesh operations, in the browser
The viewer runs meshio++'s own operations on the mesh you opened — no server, no upload, no round trip:
| operation | what it does |
|---|---|
| Quality | attaches per-cell shape metrics (mesh quality) so you can colour by them |
| Clean | welds coincident points, drops degenerate and duplicate cells (clean) |
| Smooth | Laplacian or Taubin relaxation (smoothing) |
| Refine | uniform subdivision (refine) |
| Partition | decomposes into balanced parts and colours by part (partitioning) |
| Section | the planar cross-section — the actual intersection with a plane (slice) |
| Isosurface | the level set of a point_data field (isosurface) |
They compose: apply Refine then Quality and you are looking at the quality of the refined mesh. Each one appears as a chip you can remove, Undo steps back, and Revert clears everything.
Undo is exact, not approximate. The worker keeps the original file bytes and replays the whole remaining pipeline through a single convertSurfaceOps call, so nothing needs an inverse and nothing accumulates rounding. That same call is what keeps multi-component data alive: no mesh ever crosses back into JavaScript, whose flat representation cannot carry a vector or tensor array.
Section and Isosurface are the same cutter
Both run meshio++'s marching-tetrahedra cutter and return a surface one dimension below the cells they cut — Section where the distance to a plane is zero, Isosurface where a scalar field equals the isovalue you type. They go through meshio++ rather than vtk.js's own ClipClosedSurface, which cannot colour a cut correctly: it never interpolates point data onto the cut points.
Isosurface only offers the mesh's point arrays. A cell array is piecewise constant, so it has no level set — convert it first (meshioplusplus data to-point).
Click to inspect
Inspect turns the cursor into a probe: click a cell and it reports the surface cell's id and type, every cell array's value there, the nearest vertex with its coordinates and every point array's value (all components, not just a magnitude), and — for a solid — the original volume cell the facet came from. The picked cell is outlined in the render.
Picking is a CPU ray/cell intersection, so it does not depend on the renderer, but it fires on click only, never on hover: doing it per mouse-move would make a large mesh feel broken. Above two million cells it is disabled rather than made slow.
"Original cell" only means the file when no operations are applied
The provenance array names a cell of whatever mesh the boundary was extracted from. After a refine or a section that is the operated mesh, not the file, so the row is relabelled "Cell (after ops)" rather than pretending otherwise. Chaining ids through every operation is possible but genuinely hard for marginal value.
Design
Everything routes through meshio++, so all supported formats work and neither viewer hand-rolls any rendering:
file ──► Web Worker ─────────────────────────────► main thread
meshio++ (WASM) vtk.js
readMetadata() → info panel XMLPolyDataReader
convertSurfaceOps() → VTP ─ transferred ─► mapper + DOM legend
convert() → any writable formatParsing and conversion happen in a Web Worker, and the VTP moves to the main thread as a transferable ArrayBuffer — no copy, and the UI stays responsive on a large file.
convertSurfaceOps is one call rather than readMesh → operation → writeMesh for a specific reason: the JS mesh representation is flat and cannot carry a multi-component array, so a vector field would be silently dropped on the way to the renderer. Staying inside C++ keeps it — and makes undo a replay rather than an inverse.
Running it locally
cd src/viewer
npm install
npm run dev # http://localhost:5173/meshioplusplus/viewer/See src/viewer/README.md for the build layout, and note that after changing bindings/wasm/ or src/cpp/ you must rebuild the WASM package and force npm to refresh its copy.
The offline page
view(backend="browser") writes a page with no WebAssembly in it — Python already has the C++ core, so the browser only has to render. That page cannot run operations, so anything it should be able to show is computed up front:
meshioplusplus.view(mesh, backend="browser", quality=True, color_by="temperature")quality=True bakes in per-cell metrics (off by default — it can double the page size), and geometric statistics of the volume mesh are always embedded, since the page renders only the boundary and could not derive them itself.
From the native CLI
The Python-free meshioplusplus binary can open the same Polyscope window, but only in a build configured for it:
git submodule update --init --recursive
build/configure.sh --cli --with-polyscope --build
./meshioplusplus view part.mshThe prebuilt release binaries deliberately do not include it. Their whole point is being dependency-free single files, and Polyscope needs OpenGL, GLFW and X11 — so the verbs are listed there but report the build flag instead of opening a window.
Limitations
- The offline page shows precomputed results but cannot run new operations; use the hosted viewer for that.
- Screenshots are polyscope-only. A headless vtk.js equivalent is possible but is not built.
- Polyhedron cells cannot be drawn as a volume; pass
kind="surface". - A very large mesh makes a very large HTML page in the browser backend (base64 is 1.33×).
viewwarns past 64 MB; prefer the desktop backend orcropthere.