Changelog
Changelog
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
7.0.0 - 2026-11-03
Breaking
simview terrain --areano longer prints the per-cell grids by default; add--fullto get them (see Changed below).
Added
simview sample: world-space body poses, stored velocities/forces/torques, contact counts and scalars at chosen frames (--frames 0:2000:100,--t, or 11 evenly spaced by default), as text, single-line JSON or CSV.simview inforeports value statistics per batch: scalar min/mean/max/percentiles, body world-space position bounds, and contact frame counts. It also lists episodes and the polyline count;--fulllifts the 20-entry list cap.simview terrain --areareports per-layer statistics (count, NaN count, min, p05/p50/p95, mean, max, std); with--batches A Bit adds both batches' statistics plus delta mean/RMSE/p95.--mask "known>0.5"restricts the statistics to cells passing a test.- Batches can be given by name wherever an index is taken (
terrain --batch,--batches,diff --batches,sample --batch,render --batch). simview diffaccepts several files, orfile#batchesselections, and merges them first (simview diff gt.json pred.json).--batchesis optional for a 2-batch scene. New--t0/--t1time window and--align-first-frame.terrain --along-bodyalso takes--t0/--t1.simview render:--t(repeatable; all shots in one browser session),--camera top,--track BODY,--batch,--terrain-color,--hide-ui.- Docs: "Scripts & Coding Agents" page with output shapes and a DRIFT recipe.
Changed
- A single-frame scene's playback bar shows only the screenshot button and the
format select, which then picks the screenshot's image type (PNG, JPG or
WebP): play/step, speed, the time readout, episode controls, the progress
bar and REC (a recording lasts one timeline loop, so it would be empty) are
hidden, and their shortcuts do nothing. A live run
gains them once a second frame arrives. Hiding the
recordingpanel hides such a bar entirely. simview terrain --areaprints only the sampled extent and statistics by default; the per-cell grids (andx_coords/y_coords) need--full, whose--jsonis single-line.--area --csvwithout--fullis one row per layer. Scripts reading the grids must add--full.
Fixed
- Every command exits quietly when its output pipe closes early (e.g.
| head) instead of printing aBrokenPipeErrortraceback.
6.4.1 - 2026-11-02
Changed
- Terrain property layers are no longer downloaded with the scene. Each is fetched the first time it's needed: picking its color mode, diffing it, plotting it in the Terrain profile, or hovering the terrain (the tooltip lists every property). A terrain with seven ~1.9 MB layers loads ~13 MB less up front.
- The server refuses HTTP requests whose
Hostis notlocalhostor an IP address (400), the rule the live WebSocket already applied. This blocks DNS rebinding; open the viewer by IP (e.g.http://10.0.0.5:5420), not by hostname.
Fixed
- A terrain saved with
create_terrain(compute_normals=False)draws its "normals" arrows from the normals the viewer computes for shading, instead of drawing nothing. - NaN/Infinity values are written as
null, so saved scenes, static bundles, live WebSocket messages andinfo/diff/terrain/merge--jsonoutput are valid JSON. A NaN in live mode no longer drops frames. Files already written with bareNaNstill load and merge. - A viewer that connects mid-run in live mode no longer receives duplicate
frames, and concurrent episode/batch-name updates can't leave
/modelstale. SimViewLauncherruns the same checks asshow()/save(), so undeclared attributes (velocity, force, contacts) show up in the viewer.- Merging: polylines in any input (not only the first) are rejected instead of
silently dropped; frames without
contactsmerge as empty contacts; inputs sharing a file name keep separatemetadata.sourcesentries. - Terrain sampling at a NaN position returns NaN instead of raising.
simview host:~expands the remote home directory.- Contact markers clear when a frame has an empty or shorter
contactslist. - Recording a single-frame scene captures the frame instead of an empty video.
- The scalar plot tooltip shows the episode sum/mean of the hovered batch.
- Resetting similarity colors on a point cloud without base colors removes them.
- A failed windowed-field fetch backs off for 5 s instead of retrying every frame.
- Point and contact-marker textures are disposed with their materials.
- A body omitted from some frames holds its last pose in trails, error metrics and the terrain profile instead of jumping to the origin.
- Focus mode hides other batches' copies of a singleton static mesh.
- CSV export prefixes text cells starting with
=,+,-or@with', so spreadsheets don't evaluate batch names as formulas. - The Jupyter iframe HTML escapes the
viewstring. - The Terrain profile and Error Metrics tabs need at least two frames, so a single-frame scene no longer shows empty tabs; a live run that starts on one frame gains them once it has two.
6.4.0 - 2026-10-28
Added
- Point clouds accept named color layers (
create_pointcloud(color_layers=)), selectable under "Point Color Mode", and a "Link Similarity" toggle that makes a similarity click recolor every point cloud with the same point count. - Hovering a control in the Controls panel shows a short description of it.
Changed
- Recoloring a point cloud by similarity samples a colormap lookup table instead of calling the colormap per point: about 2x faster (200k points: 19 -> 9 ms).
Fixed
- The terrain legend is hidden together with the terrain surface.
- A static scene (no body trajectories) no longer gets an empty Analysis panel or Body states window; Error Metrics and Terrain now appear only once a body has a trajectory.
- In split screen, clicking a point or terrain cell picks from the batch shown in that half.
6.3.1 - 2026-10-25
Changed
- Viewer load and idle cost, measured in headless Chromium (software GL) on generated 64-env (5000 frames) and 512-env (500 frames) recordings:
- Terrain normal arrows are built the first time they're shown, and the
per-batch lookups index the batch group directly instead of searching the
scene graph by name. 512-env load 7.2 s -> 1.7 s; scene objects 190k ->
4.1k; per-frame
updateMatrixWorld40 ms -> 1.2 ms. - The startup history walk reads only
bodyTransform(getTransformFrame), so it no longer pulls every window of every windowed field (20 -> 8 Range requests on the 64-env file with windowing forced on). /modeland/statesare requested from the page head and in parallel, overlapping the module download (64-env load at 50 ms RTT 2.43 s -> 2.04 s).- The server lists the viewer's ES modules as
modulepreloadlinks (also insimview static-vieweroutput), and the state blobs download while the scene is still being built instead of after it (64-env load at 50 ms RTT 2.10 s -> 1.81 s; no change on localhost). /staticis gzipped on the fly (2455 KB -> 570 KB for the initial assets).- The canvas redraws only on input, camera moves, playback, a new frame, or a 250 ms idle heartbeat (idle renders per 3 s: 78 -> 12), and the 60 Hz frame throttle no longer drops frames to timestamp jitter.
6.3.0 - 2026-10-22
Fixed
- The Terrain profile panel now keeps updating during live streaming: it previously froze at the frame it was opened on (the panel had no hook for newly appended states, unlike the scalar plot and error metrics). While open it now recomputes on the same throttled tick those panels use.
- A narrow race in live mode could shift a just-connected viewer's catch-up replay (and its episode boundaries) by one frame: the frame buffer and the pushed-frame counter were updated as two separate statements read by the event loop, so a catch-up could observe them mid-update. Each buffered frame now carries its own absolute index, so the offset is never reconstructed from the two.
- Serving a preloaded columnar
data=dict no longer rewrites its inline__b64__state blobs into server-local/blob/URLs in place -- the same protection the scene model already had (a latersave()of that dict would have written dead references).
Changed
simview merge's--save-mergednow writes the compact columnar states layout (same asSimulationScene.save), instead of the legacy per-frame JSON with decoded blobs -- merged files are many times smaller for the same data. Irregular scenes that can't be packed fall back to the old layout.simview inforeads the scene file once instead of twice (the whole-file gzip check and the JSON parse each read it), and summarizes scalar fields in one pass over the frames instead of two -- noticeable on the multi-hundred-MB files the command exists for.- CI/docs/publish workflow actions bumped to their newest majors:
actions/checkoutv5→v7,actions/setup-nodev4→v7,actions/upload-artifactv4→v7,actions/download-artifactv4→v8,astral-sh/setup-uvv8.3.1→v10.3.2.peaceiris/actions-gh-pages@v4andpypa/gh-action-pypi-publish@release/v1were already current. None of the upstream breaking changes affect this repo's usage (the workflows setenable-cache: trueandcache: npmexplicitly).
Security
- The live-mode
/ws/statesWebSocket now checks the browser'sOriginheader. CORS does not apply to WebSockets, so any site open in the user's browser could previously connect to a running live viewer and read every pushed frame. Localhost origins and the viewer's own page (same-origin, with the Host header naming an address) are allowed; non-browser clients, which send noOrigin, are unaffected. A live viewer reached by hostname is now refused -- reach it by address (e.g.http://10.0.0.5:5420) instead, which is what the origin check can prove is this machine. A refused connection replaces the loading splash with an error instead of hanging on it. POST /batch-namesnow rejects names longer than 256 characters, and any request body over 1 MiB is answered with 413 before it is parsed -- bounding what a rogue or runaway client can write to memory and to the on-disk sidecar.- The
/blob/URL token is now 128-bit (was 32-bit). It doubles as the only gate on blob responses, so the extra margin costs nothing. - The
data=startup hash key is now restricted to same-origin URLs. A crafted link could previously make the viewer fetch and render scene data from an arbitrary server. Thewindow.__simviewStaticBaseglobal set by the hosting page is unaffected. - Binding the server to a non-loopback host (e.g.
--host 0.0.0.0) now logs a warning that the server has no authentication; the--hosthelp text says the same.
6.2.3 - 2026-10-21
Security
- The JavaScript-error splash screen inserted the error message and URL as raw HTML; they are now rendered as plain text.
Fixed
- The point-similarity legend is now shown in scenes without terrain. Previously it was hidden there, so a recolored point cloud had no legend.
viewerDefaults.uican now setpointColorMode; it was rejected as an unknown key.- uPlot and
js-colormaps.jswere served unversioned under the immutable-cachedstatic/lib/, so updating them never reached browsers with a cached copy. uPlot now lives inlib/uPlot-1.6.32/and the locally trimmedjs-colormaps.jsinjs/vendor/(normal short cache). js-colormaps.jsnow carries its upstream MIT license notice.- The scene's duplicate directional light is folded into
LIGHTING_CONFIG(rendering unchanged).
Documentation
- Comments and docstrings unified in style and stale references fixed across the codebase; public classes and methods that lacked docstrings now have them.
- User and developer docs brought up to date with the code (terrain-similarity
clicks need Data Probe on, Error Metrics "Details" toggle, terrain diff mode,
chart click-to-seek, Body States panel, inline binary model fields, missing
test files). The docs build now runs with
--strictin CI.
6.2.2 - 2026-10-19
Removed
AnimationController.getCurrentStateIndex(),getCurrentTime(),getTotalTime()andgetFirstTime(). Read thecurrentStateIndex,currentTime,totalTimeandfirstTimeproperties instead. (This entry was added later; the release notes originally left it out.)
Security
- Batch names, episode labels, terrain layer/property names and load errors are now HTML-escaped in chart tooltips, the colour legend and the splash screen. A scene file (or a batch rename) could previously inject markup and script into the viewer page.
- Locked dependencies bumped past known advisories: starlette 1.0.0 → 1.9.0, anyio 4.13.0 → 4.14.2, idna 3.11 → 3.20.
6.2.1 - 2026-10-19
Changed
- Terrain geometry is built directly into typed arrays instead of through
THREE.PlaneGeometry, and surface colours come from a sampled colormap lookup table instead of a colormap evaluation per vertex. On a 917x1808, 2-batch terrain the terrain-build stall drops from ~1.3 s to ~0.25 s and time-to-interactive from ~3.0 s to ~1.9 s; rendering is unchanged. The unuseduvattribute is no longer allocated. - The terrain wireframe is a
LineSegmentsover the surface's position buffer with each grid edge drawn once, instead of awireframe: truemesh (which built its line index in a JS array and drew every interior edge twice); its opacity goes from 0.2 to 0.36 so the doubled-edge look is unchanged. On the same terrain the first-draw stall drops from ~1.05 s to ~0.57 s and the wireframe's per-frame GPU cost from ~24 ms to ~15 ms. - The terrain probe no longer raycasts the wireframe (only surface hits were ever used), halving the time a probe click takes (~0.83 s -> ~0.40 s).
6.2.0 - 2026-10-08
Added
- Polylines:
SimulationScene.create_polyline/model.polylinesdraw a world-space line from fixedpoints(a planned route) or from a per-frameframesblob with NaN padding (a replanned local path), with colour, dashed style and a "Show Polylines" toggle (polylinesVisibleview-link flag andviewerDefaults.uikey). - Time-ranged visibility: bodies, static objects and polylines accept
visible_ranges=[[t_from, t_to], ...](visibleRangesin the JSON) and are hidden while the playhead is outside every range. - Live WebSocket
statesmessages carry aframeOffset(absolute index of the first frame in the message), so a viewer that connects after the catch-up buffer has wrapped places episode boundaries correctly instead of shifting them by the dropped frames. SimulationScene.reconcile_available_attributes()(previously an internal step ofsave());show()andLiveViewernow call it too, so arrows and contact markers for attributes that were never declared withavailable_attributesappear in the live/show()viewer exactly as they do after save-then-view.- The Batch Legend reports when a rename could not be persisted (no file to write to
under
show()/LiveViewer/render, or the request failed) instead of silently keeping the new name. /batch-namesanswers{"ok": true, "persisted": bool}; the sidecar is keyed by the batch selection (file.json#1) as well as the path.create_terrain(compute_normals=False)stores no terrain normals (three floats per cell less in the file and over the wire);normalsis now optional in the JSON format,SimulationScene.loadandmerge_simulation_filesaccept files without it, and the viewer shades from normals it computes itself (the "normals" arrows have nothing to draw then).play=1startup hash key (view_hash(play=True)): start playing once the scene is loaded, applied aftert.viewerDefaults.panels.recording: falseand thehide=<panel,...>startup hash key (view_hash(hide=[...])) hide the REC / format / screenshot controls (and theirR/Sshortcuts) while keeping the rest of the playback bar;hide=takes anyviewerDefaults.panelsname.simview static-viewer DIRcopies the viewer page (index.html+static/) toDIR, so a site can host it as plain files next tosave_static()bundles.
Changed
SimViewModel.from_dictno longer requiresdt,collapseorstaticObjects(matching the viewer and the format doc); a wrong-lengthbatchNamesis warned about and dropped instead of rejected.create_terrainauto-computed normals use the real node spacingextent / (W - 1)(they were tilted byW / (W - 1)), andgrid_resnow means the distance between adjacent nodes, so the extent is(W - 1) * grid_res.merge_simulation_fileskeeps each input's ownbatchNamesfor its selected batches (file stem only as a fallback) and carries the first file'sviewerDefaults.- The terrain surface is shaded with the file's per-vertex
normals(recomputed when absent) instead of flat(0, 0, 1)normals, so slopes get a lighting cue. - Video recording stops after exactly one loop of content at any playback speed.
- A view-link hash change only pauses playback when it carries
t.
Fixed
save()wroteavailableAttributesinsetorder (hash-randomised per process), so identical scenes saved in two processes could fail to merge with "defines different bodies"; the order is now the attribute declaration order.save_staticexternalised the per-frame__b64__fields of a legacy-layout scene into/blob/files the viewer never fetches, leaving every body at a NaN pose.- Every
/static/*asset returned 500 when the package was installed through a symlinked path (HPC homes, macOS/tmp, symlinked venvs). - Live broadcast raised "Set changed size during iteration" and dropped the frame for the remaining clients whenever a tab connected or disconnected mid-send.
simview --host ::1(any IPv6 host) failed with "No free port found".- The explicit
simview view FILEform treatedviewas a file path. - Playback assumed the timeline starts at
t = 0: a recording whose first timestamp is 100 s spent 100 s on frame 0 per loop and mapped the progress bar wrongly. - The scalar chart froze at the catch-up chunk in live mode and warned "Invalid end index" on every later frame.
- Episode aggregates threw
RangeErrorfor an episode longer than ~125k frames. - A NaN cell in a terrain property (or diff) layer fired a blocking
alert()from the vendored colour-map library and aborted the recolour; non-finite cells are now drawn neutral grey. - Split screen in focused render mode showed a blank half until the active batch was changed, because the split batches were never pinned visible.
- Terrain-profile curves and CSV shifted left after a frame with a non-finite position; the gap is now plotted as a break.
add_stateaccepted per-batch rows that did not matchbatch_size(and a position/orientation count mismatch silently dropped rows); both now raise.simview terrain --bodysampled parented bodies in parent-local coordinates and could not find rigidly attached (local_transform) bodies at all.- A 1-D
contactstensor in a single-batch scene was written flat instead of per-batch, and merge then concatenated the index lists; an empty list raised. simview diffreported 0° orientation error for a NaN pose and never tripped--fail-on-exceedon NaN; stats are NaN-aware and NaN counts as exceeding.- Merge:
nullproperty bounds crashed withTypeError, a singleton-vs-batched static object crashed withKeyError, a flat plainheightDatalist was rejected, and a non-monotonictimelist resampled silently wrong (now a clear error). simview infoclaimed a legacy file was columnar-repackable when its blob widths were wrong, crashed on a non-columnarstatesdict, and suggested gzip for an already-gzipped file;--save-mergedinto a missing directory now creates it.- An
--areabox lying inside a single terrain cell was rejected as non-overlapping. view_hashnow matches the viewer's serialiser byte for byte for non-finite and very large numbers and for!'()*in names, and the sync test parses the key order out ofviewState.jsso drift is caught.- Model validation: primitive shape parameters must be present and positive, terrain
normal/property/embedding grids must match the heightmap, static objects loaded via
from_dictare checked for duplicate names andshapeslength, episodestart_indexpast the last frame is rejected at save time, and unknown keys in a loaded state body are ignored with a warning instead of raising. - Server: blobs registered before a columnar bail-out were kept in memory for the
server's lifetime; a ragged per-frame row crashed load instead of falling back to the
legacy layout; a blob whose byte length is not a multiple of 4 raised
struct.errorinstead ofValueError;bytes=5-3answered 416 instead of ignoring the header; the batch-names fingerprint was taken at rename time instead of load time;extract_blobsrewrotemetadata/batchNamesstrings starting with__b64__;ViewerHandleleft its server thread running after a startup timeout; a stalled live broadcast was not cancelled on timeout;SimViewServer(sim_path=[a, b])withoutdata=silently served onlya. ssh://-oProxyCommand=...and[-o...]:pathhost forms reachedsshas options; a host starting with-is now rejected in every spec form.- Viewer:
model.collapseis honoured (it was written by Python and read by nobody); atcm=colour mode the scene lacks no longer throws inside the legend and aborts the rest of the view state; the legend and surface agree on amin == maxrange; per-batch nestedembeddingDatalists are accepted; a frame withoutcontactsclears the previous frame's markers; contact markers, point arrays and trails are no longer drawn for hidden batches; probe/similarity clicks no longer hit hidden batches or fire through UI panels; Shift+Arrow no longer also rotates the camera; a NaN vector hides its arrow instead of reachingArrowHelper; the REC button no longer sticks on STOP when recording cannot start; a negative or zero speed is rejected; a paused scrub into a not-yet-fetched field window re-renders when it lands; the live "waiting for first state" splash stays until the first frame; a missing terrain reports "Terrain data is missing" instead of aTypeError. - Viewer data checks: columnar blobs are validated against
T * B * widthand known field names, a partial trailing row throws like Python does, a window fetch requires a 206 response, non-monotonic times are warned about once, and interpolation keeps extra rows from whichever side is longer. - Error metrics with a NaN frame plot a gap and print "-" instead of NaN scales; recomputation is coalesced to one pass per animation frame while the panel is open.
SimView.destroy()now disposes the scene's resize/keyboard listeners and OrbitControls; terrain and static-object disposal release their GPU resources.
6.1.2 - 2026-10-08
Changed
- The scalar plotter draws each series over the whole timeline, with a vertical line at the current playback time, instead of only the part played so far.
Fixed
- A scalar series with NaN samples (gaps in its source) no longer plots as an empty chart: the y range is taken over finite values and the gaps are drawn as breaks.
- Clicking a scalar chart (or re-selecting a batch) no longer throws the camera to the batch's grid origin and off a tracked body: changing batch now shifts the view by the offset between the two batches' cells, and body tracking re-centres on a batch switch.
6.1.1 - 2026-10-06
Added
- The view link (and
getViewState()/setViewState(),view_hash(...)) now also carries point-cloud visibility (pc), the tracked body (track), the terrain colour map (cmap) and the playback speed (speed), so a host page can hand a complete setup from one page load to the next. They are named keys, not flag bits: an absent key leaves the viewer's current setting alone, and oldv=1links decode unchanged.
Fixed
AnimationController.setSpeed()keeps the playback bar's speed dropdown in sync.- The scalar plotter no longer warns "Invalid end index" while the Analysis panel is opened before the states have loaded.
6.1.0 - 2026-10-06
Added
- The viewer can be embedded in other applications -- see the new
Embedding guide. The URL hash is now live: changing it
after load (
#v=1&t=2&cam=...) re-applies the view, so a host page can drive time, camera, batch and colour modes from outside. Two startup keys join it:#ui=0hides every floating panel, and#data=<base url>loads a static bundle instead of the server API. - The viewer instance is exposed as
window.simview(__debugSimViewstays as an alias) with a supported surface:getViewState(),setViewState(),focusBody(name)anddestroy(), plussimview:readyandsimview:frameDOM events for a host page to react to load completion and frame changes. simview.view_hash(...)builds view-link fragments (time, camera, batch, colour modes, toggle flags,data=,ui=False) from Python; stdlib-only, so it works on a base install.SimulationScene.show(view=..., height=...)opens the inline Jupyter viewer at a chosen view and iframe height;ViewerHandle.url_for(view)andLiveViewer.urlreturn full viewer URLs.SimulationScene.save_static(directory)writes a static bundle (model.json,states.json,blob/<id>) that any plain file server can host, via the stdlib-onlysimview.columnar.write_static_bundle.create_body(color=..., opacity=..., visible=...)styles a body's mesh or primitive (RGB tint, translucency) and can start it hidden; the values are stored in the scene file.SimulationScene(viewer_defaults=...)declares the viewer's initial state -- UI toggles such aspointCloudsVisible, open or closed options folders, the Body States window, and whole panels hidden -- stored asviewerDefaultsin the model.
Fixed
- Keyboard shortcuts (playback, visualization toggles, terrain probe) no longer fire while typing in a text input, textarea, select or contentEditable element, and the Body States window's Space-key handler is removed when the window is disposed.
-
The viewer page loads its stylesheets, scripts, import map and textures relative to the page, so it can be served from any path instead of only the origin root.
-
The JSON Format Specification page renders its field lists properly. Nested lists were indented too little for MkDocs, so everything under
episodes,bodies,terrainand a state'sbodiescollapsed into one run-on paragraph with literal dashes. The Notes headings no longer run into their text, and code examples no longer overflow sideways. - The developer docs no longer describe code removed in 6.0.0. The architecture
page dropped
InteractionControlsand gainedremote.py,WindowedFieldandchartControls.js;einopsis gone from the docstrings and the bug-report template. - The Live Streaming page no longer promises a late-joining viewer the full history: catch-up replays at most the most recent 10,000 frames.
6.0.1 - 2026-10-05
Fixed
- The 3D view now resizes with the browser window. The canvas kept the size it had when the page loaded while the panels and charts reflowed, so after a resize the scene came out stretched or cropped.
6.0.0 - 2026-10-01
A cleanup release: about 1,200 fewer lines of application code, plus the vendored
chroma-js and the unreachable colormap data. The scene JSON wire format is
unchanged, so scenes load in both directions. It is a major release because some
redundant Python authoring entry points were removed, listed below.
Removed
- The duplicated authoring layer.
SimulationScenenow holds the authoring logic itself. Removed:SimViewModel.create_terrain,create_body,create_static_object_singleton,create_static_object_batchedandadd_terrain, andSimulationScene.add_terrain_object,add_body_object,add_static_object_instance,create_static_object_singletonandcreate_static_object_batched. Add static objects withSimViewModel.add_static_object. SimViewBody.create_sphere/create_cylinder, which nothing called.SimViewServer.run(debug=...)andpython -m simview.server. Usesimview <file>.- Module re-exports
simview.terrain.load_scene/load_scene_model,simview.diff.load_sceneandsimview.remote.human_bytes. Import them fromsimview.utils.simview.model.BLOB_PREFIXis gone too; usesimview.columnar.BLOB_PREFIX. simview clearno longer sweepssimview_viz_*.jsontemp files or/tmp/.simview_cache. The launcher stopped writing them some time ago.- The vendored
chroma-js. Batch palettes are interpolated in CIE LCh by a small built-in function whose output is identical. - The 71 matplotlib colormaps the viewer never offered from
js-colormaps.js. Every map in the colormap dropdown is unchanged.
Changed
ViewerHandleis now the background server itself (simview.server.ViewerHandle).scene.show(),LiveViewerandsimview rendershare it instead of three copies.from simview import ViewerHandle,url, notebook display andwith-block use behave as before.simview diffsummaries gainmin, andsimview terrain --along-bodysummaries gainfinal, because both commands now share one stats helper.- A body missing
bodyTransformnow fails a merge with a clearValueErrorinstead of aKeyError. - The Body states, Analysis and Batches panels collapse with native
<details>. Collapsed, Body states and Analysis now have the same height and centring.
Fixed
- Keyboard shortcuts no longer cancel themselves out after a model reload. The controls' keydown listener was never removed, so each reload added another and every toggle fired twice.
Internal
- Python: the blob codec, state-field tables, body-name helpers, bind-host logic and
port warning each live in one place. uvicorn now chooses uvloop/httptools itself,
and the code no longer falls back for a missing
orjson, which is a hard dependency. - Frontend: the three uPlot panels share their resize, tooltip, marker and
export-bar code.
InteractionControls.jsis gone. PlaybackControls removes its listeners with a singleAbortController. View-state hashes are parsed withURLSearchParams, and existing share links still work.
5.1.1 - 2026-10-01
Fixed
- SimView's button and select styles no longer leak into pages that embed the
viewer. 5.1.0 styled every
buttonandselecton the page, including a 1px press offset (button:active { transform: translateY(1px) }) that replaced an embedder's own transform; the rules now apply only inside SimView's panels. - The Body states, Analysis and Controls panel headers share one size (13.2px); Body states used to grow with the window width.
- The docs and demo deploys no longer fail when two pushes land close together.
5.1.0 - 2026-10-01
Added
Ctoggles Show Point Clouds in scenes that have point clouds but no contacts (contacts keep the key when present); the label reads "Show Point Clouds (C)" when the key applies.
Changed
- Viewer UI restyled in Catppuccin Mocha. Every panel (Analysis, Body states,
Batches, legends, playback bar, and now the lil-gui Controls panel, which floats
inset from the corner instead of docking to the top edge) shares one
translucent surface, with Mocha blue as the accent and the same chevron as
each panel's collapse indicator. Chart axes, grids and series, body vector
arrows, probe and wireframe use the palette too; the 3D background is Mocha
mantle, so screenshots and recordings change as well. System fonts with
tabular figures for numeric readouts, visible keyboard focus rings, a larger
loading screen and a 3D-cube favicon. The playback time readout now reads
0.00 / 4.90 s. - Batch colors are the Mocha accents (blue, peach, green, mauve, …) instead of the old navy-to-cream diverging palette, whose first batch was nearly invisible on the dark background; past 14 batches the colors are interpolated between them.
- The live demo shows a new scene: GT vs prediction on helhest sample 774.
Fixed
- Stateless point clouds no longer show up as trackable bodies. A point cloud
with no per-frame states (a static prop) is now left out of the Analysis tab's
terrain-profile body picker (it could even be the default, giving an empty
profile) and the Camera "Track Body" list, matching the Body State window.
Body#hasPoseis the shared rule. - The playback time readout shows two decimals and no longer wraps onto two lines in a narrow controls row.
- Analysis scalar tabs size to their label instead of splitting the bar evenly, so with many scalars the names stay readable rather than truncating to "wh…"; the bar wraps as before.
- The Analysis panel's scalar chart no longer overflows the panel by the height of the Export CSV bar.
- The Analysis panel's Terrain chart shows its y-axis values again (a single tick step that didn't fit the short panel made uPlot drop the axis labels; the Scalars chart's coarser-step fallback is now shared) and gains time labels on its x-axis. The Error Metrics orientation label is no longer clipped, and both charts get more plot height from a slimmer x-axis strip.
5.0.0 - 2026-09-21
A cleanup release: no new features, roughly a thousand lines less code, two Python
dependencies and two vendored JavaScript libraries gone. The scene JSON wire format
and the Python authoring API (SimulationScene, SimViewBody, BodyTrajectory,
SimViewModel) are unchanged — a scene written by 4.x loads in 5.0.0 and vice
versa, and authoring code needs no edits.
Removed
- The PNG-sequence recording format. The viewer's recording dropdown offered
WEBM / MP4 / PNG; PNG captured each frame as a PNG blob and packed the run into a
.tar, which was the only reasontar.jsanddownload.jswere vendored. Both libraries are gone with it. WEBM and MP4 are unaffected, as is the single-frame PNG screenshot button, which is a separate feature. - The
jinja2dependency. It rendered sixurl_for('static', ...)calls that all resolved to the constant/static/prefix;index.htmlnow hardcodes them. einopsfrom theauthoringextra. Its fourrearrange()calls are expressible in plaintorch, whichmodel.pyalready requires. If your own code importseinopswhile relying onsimview[authoring]to install it, declare it yourself.
Changed
- The CLI now uses real argparse subcommands. Every documented invocation is
unchanged —
simview info <file>,simview diff <file> --batches 0 1,simview terrain <file> --area --layer all --json— and output, CSV/JSON rendering and the0/1/2exit codes are byte-for-byte identical. Per-subcommand--helpis now scoped, so flags no longer have to explain which subcommand they belong to. Breaking: a flag written before its subcommand (simview --json info x.json) is no longer accepted. Write it after the file, as the documentation has always shown:simview info x.json --json. - The
grayscale,heatmapandterraincolor maps now come from the bundled matplotlib maps (Greys_r,jet,terrain) instead of hand-rolled ramps. Scenes using those three names by name will look slightly different; every other map, including the defaultmagma, is unchanged. /blobRange handling now honours onlybytes=start-end, the one form the viewer sends. Suffix (bytes=-N), open-ended (bytes=N-) and multi-range requests serve the whole blob with200instead of a partial response, which RFC 9110 permits for a range a server declines. A well-formed but out-of-range request still returns416.
Internal
- The stdlib-only inspection tools (
info,diff,terrain) no longer hand-copy the same blob decoding, body-name resolution and scene loading; the shared helpers live incolumnar.pyandutils.py.terrain.py's cross-batch queries now call their single-batch counterparts instead of duplicating them. - 441 lines of CSS-in-JS moved from six UI panels into
controls.css, and the three uPlot panels share one chart-setup helper. - Dead frontend config removed (
GROUND_CONFIG,POINT_VECTOR_CONFIG,CONTACT_CONFIG,createArrows, and sixCONTROLS_CONFIGkeys that either restated three.js defaults or overwrote an OrbitControls method with a boolean).
4.2.1 - 2026-09-02
Added
- "Show Point Clouds" toggle in Body Options, for scenes that contain point-cloud bodies. Point clouds are no longer governed by Body Visualization Mode (see below), so this is how they are hidden and shown.
Fixed
- Mesh bodies no longer disappear once a run travels away from where it started. Bodies are drawn as instanced meshes positioned per instance, and THREE caches an instanced mesh's bounding sphere the first time it is frustum-tested and never invalidates it when the instances move — so every mesh body was culled wholesale as soon as its starting position left the view. Long recordings (a robot driving a few hundred metres) lost their bodies mid-playback while the terrain kept rendering.
- Selecting the "points" visualization mode no longer blanks out every mesh body.
A point-cloud body is the object itself rather than a way of drawing a body, so it no
longer contributes "points" to the mode list, and Body Visualization Mode —
noneincluded — leaves point clouds alone. In a mixed scene (a robot plus a lidar cloud) the mode offered onlypoints, which no mesh body could render. - Static point clouds are no longer listed in the Body states panel, where they showed a permanently-zero Position/Rotation. A point cloud that does carry per-frame data stays listed like any other body.
- The Analysis panel's scalar chart now actually draws. It had been painting
nothing at all: its x scale was never pinned (uPlot treats a scale's
min/maxas outputs —rangeis the pin — so the initial autoscale over empty data nulled it and no later update revisited it), the render loop redrew without rebuilding paths, so the chart kept redrawing whatever data it first saw, and the y axis offered a single tick increment that could not fit the panel's height, so uPlot drew no ticks or labels. An end-to-end test now asserts the chart paints its data and its axis rather than merely that the panel opens. - The camera's far plane and orbit limit now follow the terrain extent instead of being fixed at 500 m, so terrain and bodies on scenes hundreds of metres across no longer clip away, and the whole scene can be framed.
4.2.0 - 2026-08-31
Added
- Merging a subset of a file's batches. Append
#<batches>to any input file to contribute only some of its batches to the merged scene, e.g.simview gt.json method_a.json#1 method_b.json#1— so several files that each carry their own copy of a shared ground truth can be compared without merging that ground truth once per file. The selector takes indices (#1), comma-separated lists (#0,2), inclusive ranges (#1-3), negative indices (#-1) and the file's ownbatchNames(#ours); merged batch names keep the source file's index (run[2]). Works with--save-merged, with remote inputs, and on a single file (to view only some of its batches).merge_simulation_files(paths, selections=...)is the Python equivalent. See CLI. create_terrain(property_bounds=...)pins the color-scale range of a named terrain property explicitly, e.g.property_bounds={"friction": (0.0, 1.0)}, instead of always deriving it from that map's own min/max — so the same scale (and legend) stays comparable across scenes. Properties left out keep the data-range default, and cells outside an explicit range saturate at the end colors.- Each Error Metrics readout is prefixed with a swatch in its plot series color, so the readout doubles as a legend for the curves below it.
Fixed
- Nested fields in the Scene Info panel now start collapsed. A scene with rich metadata used to expand every nested key at once, burying the top-level entries.
4.1.0 - 2026-08-19
Added
- Remote scene files. Anywhere the CLI takes an input file (view,
info,diff,terrain,render, and multi-file merges) it now also accepts an scp-stylehost:path, fetched over the systemsshand cached locally. Transfers are compressed (gzipped on the remote, orssh -Cwhen it has nogzip), and the cache entry carries the remote file's mtime, so freshness is a plainstatcomparison. Adds--refresh/--offline, andsimview clearknows about the new cache. See CLI. - The columnar ("v4") states layout is now an on-disk format, not just a
server-side repack:
SimulationScene.save()writes it by default, producing substantially smaller files that the viewer can load without any repacking. Passcolumnar=Falsefor the previous per-frame layout, orcolumnar=Trueto raise instead of falling back when a scene is too irregular to pack. See the JSON Format Specification. simview inforeports whichstateslayout a file uses.- Episodes. An episodic (e.g. RL) recording can mark its resets with
SimulationScene.mark_episode()/LiveViewer.mark_episode(), serialized as an optionalmodel.episodesarray. The viewer ticks episode boundaries on the playback bar, adds |◀ / ▶| navigation ([/]), and overlays per-episode aggregates (including the episode return) on the scalar plots. See Episodes. - Focused-batch rendering. Above 32 batches the viewer now draws only the focused batch by default, and builds each batch's per-batch scene objects (axes, arrows, point clouds, contact points) the first time it's rendered rather than all up front — so a scene with hundreds of parallel envs loads and runs like a single-env one. Toggle with "Render All Batches" in Body Options.
- Windowed state loading. The
/blobendpoint now supports HTTP Range requests, and the viewer uses them to stream the large current-frame-only fields (velocity,angularVelocity,force,torque) in ~1 MB windows around the playhead instead of materializing whole(T, B, k)runs.bodyTransformand the scalars stay fully resident, since trails, error metrics, the terrain profile and the scalar plots all walk the entire trajectory.
Changed
SimulationScene.save()writes columnarstatesby default. Files stay readable byload,merge_simulation_files,simview info/diff/terrainand the viewer either way, but a third-party tool that parsesstatesas a JSON array will need to handle the object form (or be passedcolumnar=False).- A fully shared (singleton) terrain now ships exactly one copy of its height, normals, properties and embedding data instead of one identical copy per batch — a real saving for many-batch (e.g. RL-scale) scenes. Readers resolve the layout from the data length, so files written by older versions (which broadcast the singleton) keep working; a mixed terrain still broadcasts its shared fields.
Fixed
LiveViewer.push_stateno longer blocks the simulation loop on a slow or hung viewer tab (up to 5 s per frame before). Frames go through a bounded queue drained by a sender thread, dropping the oldest pending frame when it fills so the live view keeps tracking the simulation; dropped frames still land inscene.statesand the catch-up buffer, andstop()flushes the backlog.- The live frame buffer is now bounded (
frame_buffer_size, 10k frames by default) instead of growing for the length of a run, and a viewer connecting mid-run replays the recent window in 500-frame slices rather than onejson.dumpsof the whole history. It is registered for live broadcasts only once the replay has caught up, so a new frame can no longer overtake the history it follows. - Trails are now appended to in place instead of being rebuilt per loaded chunk, which in live mode cost O(T²) geometry reallocation over a run.
simview diffresolves parent chains and compares world poses, like the viewer's Error Metrics panel does — the two used to report different numbers for the same scene. Rigidly-attached bodies (a constantlocalTransform, absent fromstates) are now diffable and addressable via--body.- Mesh bodies authored from tensors crashed the viewer on load:
createGeometrycalled.flat()on vertices that always arrive blob-decoded as flatFloat32Arrays. Meshes are also indexed withUint32Arraynow, so more than 65535 vertices no longer wrap. - Serving an in-memory scene (
show(),LiveViewer,SimViewLauncher) no longer rewrites the caller's model in place, which left a latersave()writing dead/blob/...URLs and a secondshow()serving stale blob references. merge_simulation_filescarries terrainembeddingData(the features color mode was silently lost on merged scenes) and each model'smetadata(namespaced undermetadata.sources) through the merge, groups b64-decoded terrain normals back into per-vertex vec3s, and rejects inputs whose terrain x/y bounds differ instead of merging them into spatially misaligned batches.add_statenormalizes a 0-dim scalar tensor/array to the per-batch list every other frame stores, whichmerge_simulation_filesused to raise on.SimulationScene's internal-data cleanup frees the terrain's per-cellembedding_datatoo, potentially the largest of its arrays.- A blob fetch that returns an HTTP error now fails loudly on the load-error splash
instead of being decoded as float32 garbage, and inline
__b64__state fields decode endian-safely like the standalone blob path already did. setActiveBatchno longer forwards an out-of-range batch index to the body state window, scalar plotter and batch legend right after warning that it rejected it.- Dropped
allow_credentialsfrom the CORS config: combined with the any-localhost-port origin regex it let any other local dev server read scene data with the user's credentials. The API uses no cookies or auth headers, so nothing needed it.
Removed
- The ctrl+drag box-selection code path, which never worked — it crashed on mouseup, raycast against an always-empty list, and checked event keys that don't exist. Click handling, the data probe tooltip and shift+arrow batch switching are unaffected.
4.0.0 - 2026-08-04
⚠ Breaking changes
- Scene JSON files saved by older versions of simview will not load. Terrain's
frictionData/stiffnessDatafields and theirbounds.minFriction/maxFriction/minStiffness/maxStiffnessentries are replaced by a genericterrain.propertiesobject ({name: {data, min, max}}— see the JSON Format Specification). Re-save any existing scene file with the current version ofsimview(orSimulationScene.load()+save()) to pick up the new format; there is no automatic migration. scene.create_terrain()/SimViewModel.create_terrain()no longer acceptfriction_map=/stiffness_map=; passproperties={"friction": ..., "stiffness": ...}instead (or any other named per-cell scalar map — see below).- The batch-names sidecar file (
.<scene>.<hash>.batchnames.json) written before staleness-fingerprinting was added (pre-3.x) is no longer read; a freshPOST /batch-namesregenerates it in the current format. - The model JSON's long-superseded
batchSizefield (renamed tosimBatchesseveral releases ago) is no longer read as a fallback.
Added
- Terrain scalar properties (friction, stiffness, or any other per-cell field) are
now a fully generic, arbitrarily-named mechanism end to end — Python
(
SimViewTerrain.properties), the CLI (simview terrain --layer <name>,simview info,simview merge), and the viewer (color mode dropdown, Legend, hover/probe tooltip, Terrain Profile tab) all support any property name supplied at authoring time, with zero code changes required to add a new one. scene.create_pointcloud()now accepts optionalcolor(static per-point RGB) andembedding(per-point feature vector) tensors;scene.create_terrain()gains a matchingembedding_map(per-cell feature vector). When present, clicking a point or terrain cell recolors the whole body/grid by cosine similarity to the clicked location, computed client-side — a new "similarity" Point Color Mode for point clouds and "features" terrain color mode, both with a matching colormap legend.
Fixed
- Clicking now only recolors a point cloud/terrain by similarity when the matching mode is already selected from its dropdown; otherwise it's an ordinary selection, and a click does nothing at all unless "Data Probe" or similarity mode is active.
- The Analysis panel's "Terrain" tab no longer appears for bodies with no trajectory (e.g. a static point cloud), and now plots the whole trajectory up front instead of only revealing it progressively during playback.
- "Scene Info" now shows full metadata keys/values instead of truncating them.
3.6 - 2026-07-29
Added
SimulationScene/SimViewModelnow accept an optional free-formmetadatadict (e.g. engine name, checkpoint path, git commit, CLI args) carried through to the saved JSON,simview info, and a read-only "Scene Info" panel in the browser, so a scene stays self-describing long after it was generated.scene.create_terrain()now auto-computes normals from the heightmap gradients ifnormalsis omitted.scene.create_terrain()now acceptsgrid_resto auto-infer spatialx_lim/y_limconstraints instead of requiring manual definition.simview terrain <file> --along-body BODY: sample terrain layer(s) bilinear-interpolated at a body's per-frame (x, y) position — "what terrain is under the robot's driven path". With--batches A B, both batches' terrains are sampled along batch A's (reference, typically ground-truth) trajectory and reported asvalue_a/value_b/deltaper layer, so the delta reflects property differences under the path rather than trajectory divergence. Honors--layer/--everyand the usual--json/--csvoutput modes.- "Terrain" tab in the browser Analysis panel: plots a terrain layer
(height/friction/stiffness) sampled under a body's path over time, one
series per batch, with layer/body pickers, a path picker ("own path"
per batch, or every batch's terrain along one reference batch's path —
e.g. ground truth's), playback-synced reveal, click-to-seek, and CSV
export. The browser-side counterpart of
simview terrain --along-body. --fail-on-exceedflag forsimview diff: exits with code 2 (after printing the normal report) when any diffed body's trajectory exceeds--pos-threshold/--rot-threshold-deg, and 0 when within them -- distinct from the usage/parse-error exit 1, so scripts and CI can use an exported scene as a regression tripwire.
Fixed
- Focusing a batch while a Scalars plot was open threw
s.stroke is not a functionon the next redraw (uPlot expectsseries.stroketo stay a function; the focus handler was overwriting it with a color string).
3.5 - 2026-07-28
Added
simview render <file> --output frame.png: headless PNG screenshot via a real (headless) browser driving a realSimViewServerinstance, with--view/--width/--heightoptions. Ships as a new optionalrenderextra rather than a hard dependency.- Terrain diff color overlay: a "diff" color mode (diverging colormap centered on zero) plus Diff Layer/Batch A/Batch B pickers in Terrain Options, with a matching diverging colorbar in the Legend.
- The terrain data probe now shows every batch's height/friction/stiffness at the hovered cell, plus each one's delta from a reference batch, instead of just the hovered batch.
--per-axisflag forsimview diff, reporting signederr_x/err_y/err_z(batch A minus batch B) per frame in--json/--csvoutput.- Mean/min
|delta|stats (alongside the existing max) insimview terrain --batches --areaoutput. - Full documentation site (MkDocs + mkdocs-material + mkdocstrings) covering
usage, the CLI, the JSON format specification, the API reference, and a
developer guide, published to GitHub Pages.
README.mdis trimmed to a landing page that links to it.
Changed
- The Error Metrics panel auto-selects a sensible Batch A/B default from batch names (e.g. ground-truth vs. post-adaptation) instead of always defaulting to indices 0/1, falling back to 0/1 when no batch name matches.
3.4 - 2026-07-27
Added
simview info <file>: a structural summary of a scene JSON (model/terrain/ body/state breakdown, columnar-repack eligibility, consistency warnings) in human-readable text or--json.simview terrain <file> --point/--area: numeric height/friction/stiffness queries (bilinear-interpolated at a point, or a raw grid over an area), plus--batches A Bto compare two batches (value_a/value_b/deltaper layer).simview diff <file> --batches A B: per-frame position/orientation divergence between two batches' trajectories, with--body/--every/--pos-threshold/--rot-threshold-degoptions.--csvoutput forsimview diffandsimview terrain, alongside the existing--json.
Changed
- Only emit uvicorn's access log when running in debug mode.
3.3 - 2026-07-17
Added
- Expose the installed package version as
simview.__version__. - Test against Python 3.14 in CI and advertise it in the package classifiers.
- Dependabot configuration for GitHub Actions, npm, and Python dependencies.
Changed
- Bump the PyPI development-status classifier to
5 - Production/Stable. - Use uvicorn's modern sansio websocket implementation for the live server when
available, silencing the
websockets.legacydeprecation warning. - Raise the CI coverage floor from 80% to 83%.
Removed
- Unused
collapsedMode/focusedModeplaceholder flags fromBatchManager.
3.2 - 2026-07-15
Added
- Shareable view links: the current camera/playback state is encoded in the URL hash so a view can be restored or handed off.
- Single-frame PNG screenshot export.
Changed
- Prepare packaging for PyPI publishing (metadata, build, publish workflow).
- Replace CCapture with the browser-native
MediaRecorderfor video recording, covered by an e2e test.
Fixed
- Unblock CI: guard the optional
numpyimport and add--no-launchtoexample.py. - Plot visualization fixes.
3.1 - 2026-07-14
Added
- Live streaming mode:
LiveViewerpushes states to connected browser tabs over WebSocket as a simulation runs. - Non-blocking
scene.show()with Jupyter iframe support (_repr_html_). - Smooth interpolated playback (position lerp + quaternion slerp) with a toggle.
- Error-metric summary stats and CSV export in the analysis panel.
Changed
- Vendor three.js and chroma-js locally so the viewer works fully offline.
- Serve states as per-body whole-trajectory binary columns ("v4" columnar
repack), backed by a
Float32ArrayStateStore, for much cheaper playback of long recordings.
Fixed
- Binary-search seek for non-uniform timelines, parallel blob fetches, and versioned immutable blob URLs.
Testing / infrastructure
- Add vitest + Playwright frontend tests, pyright type checking, and a CI coverage floor.
3.0 - 2026-07-13
Baseline release. Highlights of the surface established by this version:
- Authoring API —
SimulationScenewith incremental model building,add_state/add_trajectory(batched, binary-encoded), gzip support, and JSON save/load. - Wire format — HTTP-served
model/states, binary-encoded numeric fields, parent-relative (rigid and articulated) body transforms, grouped body names. - Frontend — vanilla-JS/THREE.js viewer with batched split-screen comparison, camera tracking, trajectory trails, terrain data probe, a unified Analysis panel (Scalars + Error Metrics, plotted with uPlot), and synchronized timeline scrubbing.
- Tooling — CLI (
simviewview /clear/--save-merged), multi-file merge pipeline, CORS-hardened server with cache headers,py.typed, and CI across Python 3.12/3.13 with a base-install-only check.