Architecture Overview
SimView has two halves that only talk to each other over HTTP and WebSocket:
a Python backend (FastAPI/uvicorn) that owns a scene's data — a static
model plus time-ordered states — and a vanilla-JS/THREE.js frontend that
renders it in the browser. Neither side needs to know the other's internals
beyond the JSON wire format they agree on.
Python (authoring or file-on-disk) Browser
┌───────────────────────────┐ HTTP GET ┌───────────────────────────┐
│ SimulationScene / JSON │ ───────────▶ │ SimView.js (main.js) │
│ - model (static) │ /model │ - loadData/fetchBlobs │
│ - states (per-frame) │ /states, │ - StateStore, Scene, │
│ │ /blob/... │ AnimationController │
│ SimViewServer (FastAPI) │ ◀──────────── │ - Controls, panels │
│ - columnar repack │ WebSocket │ │
│ - LiveViewer push_state │ (live only) │ │
└───────────────────────────┘ └───────────────────────────┘
Python backend (simview/)
model.py— static scene definition:SimViewModel,SimViewBody(shape + optionalparent/localTransformfor rigid attachment),SimViewStaticObject,SimViewTerrain. Pure data/validation, no torch dependency at this layer beyond what's passed in.state.py— per-frame dynamic data:SimViewBodyState(one frame) andBodyTrajectory(a whole(T, B, ...)trajectory in one call, authoring-only, needs torch/numpy). Both can binary-encode numeric fields (__b64__+ little-endian float32) instead of plain JSON lists — see JSON Format Specification.scene.py—SimulationScene, the main authoring API: builds a model incrementally (create_terrain,create_body, ...), accumulatesstatesviaadd_state/add_trajectory, and cansave()/load()JSON (optionally gzipped) orshow()a non-blocking viewer (returns aViewerHandle, usable in Jupyter via_repr_html_).server.py—SimViewServer: FastAPI app servingtemplates/index.htmlandstatic/, plus/modeland/states(or per-blob endpoints). On load, it tries to repack the legacy per-framestatesarray into a columnar "v4" payload — one binary blob per body per field covering the whole trajectory, fetched in parallel asFloat32Arrays — for much cheaper playback of long recordings; it transparently falls back to serving the legacy per-frame array if the frames aren't uniform enough to columnarize. Also handles WebSocket live-push (seelive.py) and batch-rename persistence.live.py—LiveViewer: starts the server immediately (on a background thread via_ThreadedServer) and streamspush_statecalls to connected browser tabs over WebSocket as a simulation runs, instead of saving-then-viewing after the fact.launcher.py—SimViewLauncher: blocking launch used by the CLI /save+view workflows (as opposed tolive.py's streaming launch orscene.show()'s non-blocking one).merge.py—merge_simulation_files: combines multiple scene JSON files (e.g. a real-world recording and a simulated rerun) that share the same bodies/terrain into one scene with extra batches, resampling every file but the first onto the first file's timeline by nearest timestamp.diff.py— per-frame position/orientation divergence between two batches, backingsimview diff.terrain.py— bilinear-interpolated point/area terrain queries, backingsimview terrain.info.py— structural summary (body/terrain/state breakdown, consistency warnings), backingsimview info.render.py— headless PNG screenshots via a real (headless) browser driving a realSimViewServerinstance, backingsimview render.utils.py— small shared helpers (e.g. free-port lookup, gzip-transparent file reads).__main__.py— CLI entry point (simviewscript): view file(s),simview info/terrain/diff/render,simview clearcache cleanup,--save-merged.
Lazy imports
simview/__init__.py only imports authoring symbols (SimulationScene,
SimViewBody, etc.) on first attribute access, via a module-level __getattr__
that looks each name up in a _LAZY_EXPORTS table. This keeps import simview
torch-free for viewing-only installs — a viewing-only install can import simview
and use SimViewServer/CLI features without ever needing torch/einops/numpy
installed, and only pays that import cost (and dependency requirement) the moment
an authoring symbol like SimulationScene is actually touched.
Frontend (simview/static/js/)
Vanilla JS ES modules (no bundler/framework), loaded via templates/index.html's
importmap. Entry point main.js → SimView.js (SimView class), which owns startup
(loadData/fetchBlobs/initFromModel) and wires everything else together:
components/—Scene(THREE.js scene/camera/renderer),StateStore(decoded trajectory data + playback lookups),AnimationController(playback loop, speed, interpolation),BatchManager(per-batch color/focus/visibility),InteractionControllerInteractionControls(camera/mouse/keyboard).objects/— THREE.js object wrappers:Body,StaticObject,Terrain(heightfield mesh + friction/stiffness/click-to-similarity "features" color modes), plus shared helpers inutils.js.colormap.js/similarity.jsfactor the colormap resolver and cosine-similarity math out ofutils.js(which pulls in the browser-onlychromapackage) into small, dependency-light modules used by bothBody's click-to-similarity point coloring andTerrain's "features" mode.ui/— DOM-based UI panels:Controls(main options panel),PlaybackControls,BodyStateWindow,Legend/BatchLegend,ScalarPlotterandErrorMetrics(both behindAnalysisPanel's tab switcher, both plotted with vendored uPlot).utils/— pure logic factored out for unit testing without a DOM/THREE.js:blobCodec.js(decode the server's columnar float32 blobs — must stay in sync with the server's repack logic),bodyTransforms.js(resolve parent-relative poses,topoSortBodies),interpolate.js,errorMath.js,csv.js,viewState.js(encode/decode the shareable view-link URL hash),liveFollow.js(should new live frames auto-scroll playback),terrainSample.js(bilinear terrain layer sampling for the Analysis panel's Terrain tab, plushasBodyTrajectorygating whether that tab shows up for a given body).
Vendored third-party libraries
uPlot (MIT), three.js
(MIT), and chroma-js (MIT) are vendored under
simview/static/lib/ (version-stamped directories, e.g. lib/three-0.174.0/) rather
than loaded from a CDN, so the viewer works fully offline. All third-party libraries
used by SimView are permissively licensed (MIT/BSD), so there are no licensing
restrictions on commercial use. Don't add new CDN-loaded dependencies without a reason
to break that pattern.
Wire format
Python and JavaScript agree on the model/states JSON shape, binary field encoding,
parent-relative bodies, and the columnar repack independently — there is no shared
schema file. See JSON Format Specification for the full contract;
read it before changing anything that touches serialization on either side
(model.py/state.py/server.py in Python, blobCodec.js/bodyTransforms.js in JS).
Project structure
simview/
├── simview/ # Python package
│ ├── model.py # SimViewModel, SimViewBody, SimViewStaticObject, SimViewTerrain
│ ├── state.py # SimViewBodyState, BodyTrajectory
│ ├── scene.py # SimulationScene, ViewerHandle
│ ├── server.py # SimViewServer (FastAPI app, columnar repack)
│ ├── live.py # LiveViewer, _ThreadedServer
│ ├── launcher.py # SimViewLauncher
│ ├── merge.py # merge_simulation_files
│ ├── diff.py # simview diff backend
│ ├── terrain.py # simview terrain backend
│ ├── info.py # simview info backend
│ ├── render.py # simview render backend (Playwright)
│ ├── utils.py # shared helpers
│ ├── __main__.py # simview CLI entry point
│ ├── templates/ # index.html
│ └── static/
│ ├── css/
│ ├── js/
│ │ ├── main.js # entry point
│ │ ├── SimView.js # top-level SimView class
│ │ ├── components/
│ │ ├── objects/
│ │ ├── ui/
│ │ └── utils/
│ ├── lib/ # vendored three.js, chroma-js, uPlot, ...
│ └── textures/
├── tests/ # pytest suite (+ tests/js/ vitest, tests/e2e/ Playwright)
├── example.py # authoring example (see Quick Start)
├── example_live.py # LiveViewer example
├── mkdocs.yml # this documentation site
├── docs/ # documentation source
└── pyproject.toml # package metadata, ruff/pyright config, uv dependency-groups