Embedding SimView in Another Application
SimView's viewer is a single page that can be dropped into another web app as an
<iframe>, pointed at data you serve yourself, stripped of its own panels, and driven
from the host page. The DRIFT dataset viewer embeds it this way.
The embedding unit is the iframe
The page assumes it owns the whole window (the canvas and every panel are sized to
the viewport), so embed it as an <iframe> rather than mounting it into a <div>. An
iframe also scopes keyboard shortcuts to the viewer: they only fire while it has focus,
and never while the user types in a form field.
<iframe src="http://127.0.0.1:5420/#v=1&t=2.5&ui=0" style="border:0;width:100%;height:600px"></iframe>
From Python, scene.show(view=..., height=...) returns a handle whose _repr_html_
is exactly that iframe, and handle.url_for(view) gives the URL for your own markup.
simview.view_hash(...) builds the fragment:
from simview import view_hash
handle = scene.show(view=view_hash(t=2.5, cam=(8, 8, 4), tgt=(0, 0, 0), ui=False), height=480)
handle.url_for(view_hash(batch=1, terrain_color_mode="friction"))
URL hash: the control surface
Everything the "Copy view link" button serialises can be set in the fragment, and
the viewer re-applies the fragment whenever it changes — so a host page controls
time, camera, batch and colour modes after load by assigning iframe.src's hash (or
location.hash inside the frame), with no JavaScript access to the viewer needed.
| Key | Meaning |
|---|---|
v=1 |
view-state version; required for the keys below |
t |
playback time in seconds (pauses playback and seeks; the only key that pauses) |
cam, tgt |
camera position and orbit target, x,y,z |
fov |
camera field of view in degrees |
b |
focused batch index |
bvm, tcm |
body visualization mode, terrain colour mode |
flags |
bitmask of the boolean toggles (view_hash(flags={...}) builds it) |
pc |
point clouds visible, 0 or 1 |
track |
body to keep the camera on (a body name, or None) |
cmap |
terrain colour map name |
speed |
playback speed multiplier |
Every key is optional and an absent key leaves the viewer's current setting alone, so a
fragment can carry a whole setup between page loads (getViewState() → serializeViewState
on one page, the hash on the next) or change one thing.
Four startup keys are read once, at load, and don't need v=1:
| Key | Meaning |
|---|---|
data=<base> |
static mode: fetch model.json, states.json and blob/<id> from <base> instead of the server API (see below) |
ui=0 |
hide every floating panel (controls, playback bar, legends, Body States, Analysis); the 3D view and keyboard shortcuts stay |
play=1 |
start playing once the scene is loaded (applied after t, so #v=1&t=30&play=1 seeks to 30 s and plays from there) |
hide=<panel,...> |
hide named panels, same names as viewerDefaults.panels (e.g. hide=recording drops REC, format and screenshot from the playback bar) |
Serving the data yourself: static bundles
A scene can be written as flat files any static file server can host — no SimView server process at all:
and loaded with #data=/scenes/run42. The base is an ordinary URL resolved against the
viewer page, so a page-relative #data=../scenes/run42 works too, and the scenes can
live anywhere on the site. (The hash is attacker-controllable via a crafted link,
so a data= pointing at another origin is ignored with a console warning; a page
that deliberately wants cross-origin data sets window.__simviewStaticBase
before the viewer's scripts load.) The viewer page itself is vendored with
It loads all of its assets relative to the page, so it can be served from any path of
your origin. The GitHub Pages demo serves the same static layout
(.github/workflows/demo.yml dumps it from a running server);
simview.columnar.write_static_bundle is the stdlib-only writer behind save_static.
Serving SimView's own assets from your app
simview static-viewer output is ready to host. An app that instead serves the
package's templates/index.html and static/ itself (say, from a FastAPI app next to
its bundles) misses two things SimView's server does, both of which help on a slow link:
- Module preloads.
index.htmlhas a<!-- @modulepreload -->placeholder. Servesimview.preload.render_index(index_path, package_dir)instead of the raw file, so the browser fetches the module graph (~50 files) in parallel instead of one import level at a time. - Gzip on
/static. ~2.4 MB of JS shrinks to ~570 KB. Compress/staticonly: blobs are float data that barely compresses, and the viewer range-fetches them.
from importlib.resources import files
from fastapi.responses import HTMLResponse
from starlette.middleware.gzip import GZipMiddleware
from starlette.staticfiles import StaticFiles
from simview.preload import render_index
pkg = files("simview")
app.mount("/simview/static", GZipMiddleware(StaticFiles(directory=str(pkg / "static")), minimum_size=1024))
@app.get("/simview/")
def viewer():
return HTMLResponse(render_index(str(pkg / "templates" / "index.html"), str(pkg)))
Terrain property layers are fetched when first needed rather than at load, so a host
sees those blob/<id> requests arrive later, while the user explores the scene.
Declare how the viewer should start at authoring time instead of patching it after
load — see viewerDefaults and the per-body color/opacity/visible fields in the
JSON format spec:
scene = SimulationScene(..., viewer_defaults={
"ui": {"pointCloudsVisible": False, "terrainColorMode": "friction"},
"folders": {"Camera Options": True, "Terrain Options": False},
"bodyStatesOpen": False,
"panels": {"analysis": False},
})
scene.create_body("ghost", BodyShapeType.BOX, color=(1.0, 0.7, 0.28), opacity=0.55, hx=..., hy=..., hz=...)
Driving the viewer from JavaScript
A same-origin host page can reach the instance as iframe.contentWindow.simview (in
the page itself: window.simview). The supported surface:
simview:ready event on window |
the scene is loaded and rendering; detail.simview is the instance. In live mode it fires before the first frame arrives |
simview:frame event on window |
the displayed frame changed; detail is { index, time } |
getViewState() / setViewState(state) |
the object behind the view link, read or applied without touching location.hash |
focusBody(name, batchIndex?) |
re-centre the orbit camera on a body, keeping the viewing angle; false until the body has a position |
animationController.play() / pause() / goToTime(t) / seekToIndex(i) / setSpeed(x) |
playback |
animationController.currentStateIndex / currentTime / firstTime / totalTime / isPlaying |
playback state, read-only (currentTime and firstTime are absolute, totalTime is a duration; the get...() getters were removed in 6.2.2) |
uiControls.findController(name).setValue(v) |
flip a control so the widget and the scene stay in sync (showPointClouds, trackBody, colorMode, ...) |
destroy() |
stop the render loop, close the live socket and release the renderer |
const frame = document.querySelector("iframe");
frame.contentWindow.addEventListener("simview:ready", ({ detail: { simview } }) => {
simview.focusBody("robot");
simview.animationController.play();
});
frame.contentWindow.addEventListener("simview:frame", ({ detail }) => showImageFor(detail.index));
window.__debugSimView remains as an alias of window.simview for older code.
Headless figures
simview render --view "#v=1&t=2&cam=..." (and simview.render.render_screenshot)
takes the same fragment, so a figure's view can be built with view_hash and rendered
through the same pipeline the interactive viewer uses.